From 418d6b08b0f7720a84860eeec0e5bc285b97945d Mon Sep 17 00:00:00 2001 From: Topvennie Date: Tue, 10 Dec 2024 01:04:19 +0100 Subject: [PATCH] feat(progress): add a progression bar --- tui/components/progress/progress.go | 125 ++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 tui/components/progress/progress.go diff --git a/tui/components/progress/progress.go b/tui/components/progress/progress.go new file mode 100644 index 0000000..3c123ea --- /dev/null +++ b/tui/components/progress/progress.go @@ -0,0 +1,125 @@ +// Package progress provides an animated progress bar +package progress + +import ( + "strings" + "sync/atomic" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "go.uber.org/zap" +) + +var lastID int64 + +func nextID() int64 { + return atomic.AddInt64(&lastID, 1) +} + +// FrameMsg is a message that is sent on every progress frame tick +type FrameMsg struct { + id int64 +} + +// StartMsg is a message that starts the progress bar +type StartMsg struct { + width int + widthTarget int + interval time.Duration +} + +// Model for the progress component +type Model struct { + id int64 + width int + widthTarget int + interval time.Duration + styleFainted lipgloss.Style + styleGlow lipgloss.Style +} + +// New creates a new progress +func New(styleFainted, styleGlow lipgloss.Style) Model { + zap.S().Info(styleFainted) + return Model{id: nextID(), styleFainted: styleFainted, styleGlow: styleGlow} +} + +// Init initializes the progress component +func (m Model) Init() tea.Cmd { + zap.S().Info(m.styleFainted) + return nil +} + +// Start starts a progress bar until it reaches a given width in a given duration +func (m Model) Start(width int, runningTime time.Duration, duration time.Duration) tea.Cmd { + zap.S().Info(m.styleFainted) + return func() tea.Msg { + interval := (duration / 2) / time.Duration(width) + + return StartMsg{ + width: int(runningTime / interval), + widthTarget: width * 2, + interval: interval, + } + } +} + +// Update handles the progress frame tick +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + zap.S().Info(m.styleFainted) + switch msg := msg.(type) { + case FrameMsg: + if msg.id != m.id { + return m, nil + } + + m.width++ + + if m.width < m.widthTarget { + return m, tick(m.id, m.interval) + } + + return m, nil + + case StartMsg: + m.id = nextID() + m.width = msg.width + m.widthTarget = msg.widthTarget + m.interval = msg.interval + + return m, tick(m.id, m.interval) + } + + return m, nil +} + +// View of the progress bar component +func (m Model) View() string { + zap.S().Info(m.styleFainted) + glowCount := min(20, m.width) + // Make sure if m.width is uneven that the half block string is in the glow part + if m.width%2 == 1 && glowCount%2 == 0 { + glowCount-- + } + faintedCount := m.width - glowCount + + // Construct fainted + fainted := strings.Repeat("▄", faintedCount/2) + fainted = m.styleFainted.Render(fainted) + + // Construct glow + glow := strings.Repeat("▄", glowCount/2) + if glowCount%2 == 1 { + glow += "▖" + } + glow = m.styleGlow.Render(glow) + + return lipgloss.JoinHorizontal(lipgloss.Top, fainted, glow) +} + +func tick(id int64, interval time.Duration) tea.Cmd { + return tea.Tick(interval, func(_ time.Time) tea.Msg { + return FrameMsg{id: id} + }) +}