feat(cli): add ProgressBar with Increment, Set, SetMessage, Done
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
50afecea6d
commit
175ad1e361
2 changed files with 166 additions and 0 deletions
106
pkg/cli/progressbar.go
Normal file
106
pkg/cli/progressbar.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ProgressHandle controls a progress bar.
|
||||
type ProgressHandle struct {
|
||||
mu sync.Mutex
|
||||
current int
|
||||
total int
|
||||
message string
|
||||
width int
|
||||
}
|
||||
|
||||
// NewProgressBar creates a new progress bar with the given total.
|
||||
func NewProgressBar(total int) *ProgressHandle {
|
||||
return &ProgressHandle{
|
||||
total: total,
|
||||
width: 30,
|
||||
}
|
||||
}
|
||||
|
||||
// Current returns the current progress value.
|
||||
func (p *ProgressHandle) Current() int {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.current
|
||||
}
|
||||
|
||||
// Total returns the total value.
|
||||
func (p *ProgressHandle) Total() int {
|
||||
return p.total
|
||||
}
|
||||
|
||||
// Increment advances the progress by 1.
|
||||
func (p *ProgressHandle) Increment() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.current < p.total {
|
||||
p.current++
|
||||
}
|
||||
p.render()
|
||||
}
|
||||
|
||||
// Set sets the progress to a specific value.
|
||||
func (p *ProgressHandle) Set(n int) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if n > p.total {
|
||||
n = p.total
|
||||
}
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
p.current = n
|
||||
p.render()
|
||||
}
|
||||
|
||||
// SetMessage sets the message displayed alongside the bar.
|
||||
func (p *ProgressHandle) SetMessage(msg string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.message = msg
|
||||
p.render()
|
||||
}
|
||||
|
||||
// Done completes the progress bar and moves to a new line.
|
||||
func (p *ProgressHandle) Done() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.current = p.total
|
||||
p.render()
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// String returns the rendered progress bar without ANSI cursor control.
|
||||
func (p *ProgressHandle) String() string {
|
||||
pct := 0
|
||||
if p.total > 0 {
|
||||
pct = (p.current * 100) / p.total
|
||||
}
|
||||
|
||||
filled := 0
|
||||
if p.total > 0 {
|
||||
filled = (p.width * p.current) / p.total
|
||||
}
|
||||
if filled > p.width {
|
||||
filled = p.width
|
||||
}
|
||||
empty := p.width - filled
|
||||
|
||||
bar := "[" + strings.Repeat("\u2588", filled) + strings.Repeat("\u2591", empty) + "]"
|
||||
|
||||
if p.message != "" {
|
||||
return fmt.Sprintf("%s %3d%% %s", bar, pct, p.message)
|
||||
}
|
||||
return fmt.Sprintf("%s %3d%%", bar, pct)
|
||||
}
|
||||
|
||||
// render outputs the progress bar, overwriting the current line.
|
||||
func (p *ProgressHandle) render() {
|
||||
fmt.Printf("\033[2K\r%s", p.String())
|
||||
}
|
||||
60
pkg/cli/progressbar_test.go
Normal file
60
pkg/cli/progressbar_test.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProgressBar_Good_Create(t *testing.T) {
|
||||
pb := NewProgressBar(100)
|
||||
require.NotNil(t, pb)
|
||||
assert.Equal(t, 0, pb.Current())
|
||||
assert.Equal(t, 100, pb.Total())
|
||||
}
|
||||
|
||||
func TestProgressBar_Good_Increment(t *testing.T) {
|
||||
pb := NewProgressBar(10)
|
||||
pb.Increment()
|
||||
assert.Equal(t, 1, pb.Current())
|
||||
pb.Increment()
|
||||
assert.Equal(t, 2, pb.Current())
|
||||
}
|
||||
|
||||
func TestProgressBar_Good_SetMessage(t *testing.T) {
|
||||
pb := NewProgressBar(10)
|
||||
pb.SetMessage("Processing file.go")
|
||||
assert.Equal(t, "Processing file.go", pb.message)
|
||||
}
|
||||
|
||||
func TestProgressBar_Good_Set(t *testing.T) {
|
||||
pb := NewProgressBar(100)
|
||||
pb.Set(50)
|
||||
assert.Equal(t, 50, pb.Current())
|
||||
}
|
||||
|
||||
func TestProgressBar_Good_Done(t *testing.T) {
|
||||
pb := NewProgressBar(5)
|
||||
for i := 0; i < 5; i++ {
|
||||
pb.Increment()
|
||||
}
|
||||
pb.Done()
|
||||
// After Done, Current == Total
|
||||
assert.Equal(t, 5, pb.Current())
|
||||
}
|
||||
|
||||
func TestProgressBar_Bad_ExceedsTotal(t *testing.T) {
|
||||
pb := NewProgressBar(2)
|
||||
pb.Increment()
|
||||
pb.Increment()
|
||||
pb.Increment() // Should clamp to total
|
||||
assert.Equal(t, 2, pb.Current())
|
||||
}
|
||||
|
||||
func TestProgressBar_Good_Render(t *testing.T) {
|
||||
pb := NewProgressBar(10)
|
||||
pb.Set(5)
|
||||
rendered := pb.String()
|
||||
assert.Contains(t, rendered, "50%")
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue