Skip to content

Commit e162694

Browse files
committed
feat: add progress bar to TUI runs panel
1 parent 4d01cd4 commit e162694

File tree

3 files changed

+79
-2
lines changed

3 files changed

+79
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **TUI progress bar**: Shows a progress bar in the Runs panel when a job is running
13+
- Displays elapsed time vs average duration with Unicode gradient bar (`▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒`)
14+
- Only shown when the job has historical run data (needs average duration)
15+
1216
- **Stuck job detection**: `gob run` and `gob await` now detect potentially stuck jobs and return early
1317
- Timeout: average successful duration + 1 minute (or 5 minutes if no historical data)
1418
- Triggers when: elapsed time exceeds timeout AND no output for 1 minute

internal/tui/styles.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,14 @@ var (
195195
jobTimeSelectedStyle = lipgloss.NewStyle().
196196
Foreground(colorCyan).
197197
Background(selectionBg)
198+
199+
// Progress bar styles
200+
progressBarFillStyle = lipgloss.NewStyle().
201+
Foreground(successColor)
202+
203+
progressBarEmptyStyle = lipgloss.NewStyle().
204+
Foreground(colorBrightBlack)
205+
206+
progressBarTextStyle = lipgloss.NewStyle().
207+
Foreground(colorBrightBlack)
198208
)

internal/tui/tui.go

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,9 @@ func (m Model) Init() tea.Cmd {
229229
)
230230
}
231231

232-
// logTickCmd returns a command that sends a tick every 500ms for log updates
232+
// logTickCmd returns a command that sends a tick every second for log updates
233233
func logTickCmd() tea.Cmd {
234-
return tea.Tick(500*time.Millisecond, func(t time.Time) tea.Msg {
234+
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
235235
return logTickMsg(t)
236236
})
237237
}
@@ -1654,6 +1654,13 @@ func (m Model) renderRunsList(width int) string {
16541654
// Stats summary line
16551655
statsLine := m.formatStatsLine()
16561656
lines = append(lines, mutedStyle.Render(statsLine))
1657+
1658+
// Progress bar (only shown when job is running with avg duration available)
1659+
progressBar := m.renderProgressBar(width)
1660+
if progressBar != "" {
1661+
lines = append(lines, progressBar)
1662+
}
1663+
16571664
lines = append(lines, "")
16581665

16591666
// Calculate column widths based on panel width
@@ -2003,6 +2010,62 @@ func (m Model) formatStatsLine() string {
20032010
avgDuration)
20042011
}
20052012

2013+
// renderProgressBar renders a progress bar for a running job
2014+
// Returns empty string if no progress bar should be shown
2015+
func (m Model) renderProgressBar(width int) string {
2016+
// Only show progress bar when:
2017+
// 1. We have a selected job that is running
2018+
// 2. We have stats with an average duration
2019+
if len(m.jobs) == 0 || m.jobScroll.Cursor >= len(m.jobs) {
2020+
return ""
2021+
}
2022+
2023+
job := m.jobs[m.jobScroll.Cursor]
2024+
if !job.Running {
2025+
return ""
2026+
}
2027+
2028+
if m.stats == nil || m.stats.AvgDurationMs <= 0 {
2029+
return ""
2030+
}
2031+
2032+
// Calculate elapsed time
2033+
elapsed := time.Since(job.StartedAt)
2034+
avgDuration := time.Duration(m.stats.AvgDurationMs) * time.Millisecond
2035+
2036+
// Calculate progress percentage (capped at 100%)
2037+
progress := float64(elapsed) / float64(avgDuration)
2038+
if progress > 1.0 {
2039+
progress = 1.0
2040+
}
2041+
2042+
// Calculate bar dimensions
2043+
// Reserve space for: percentage (5) + space (1) + times display (~20)
2044+
barWidth := width - 26
2045+
if barWidth < 10 {
2046+
barWidth = 10
2047+
}
2048+
2049+
filledWidth := int(float64(barWidth) * progress)
2050+
emptyWidth := barWidth - filledWidth
2051+
2052+
// Build the bar using Unicode block characters
2053+
// ▓ (U+2593) for filled, ▒ (U+2592) for empty
2054+
filled := progressBarFillStyle.Render(strings.Repeat("▓", filledWidth))
2055+
empty := progressBarEmptyStyle.Render(strings.Repeat("▒", emptyWidth))
2056+
2057+
// Format elapsed / avg time
2058+
elapsedStr := formatDuration(elapsed)
2059+
avgStr := formatDuration(avgDuration)
2060+
timeInfo := progressBarTextStyle.Render(fmt.Sprintf("%s / %s", elapsedStr, avgStr))
2061+
2062+
// Format percentage
2063+
pct := int(progress * 100)
2064+
pctStr := progressBarTextStyle.Render(fmt.Sprintf("%3d%%", pct))
2065+
2066+
return fmt.Sprintf("%s%s %s %s", filled, empty, pctStr, timeInfo)
2067+
}
2068+
20062069
// formatRelativeTime formats a time as a relative duration from now
20072070
func formatRelativeTime(t time.Time) string {
20082071
d := time.Since(t)

0 commit comments

Comments
 (0)