feat(ui): add Progress interface with Quiet and Interactive implementations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cc8baa4d78
commit
43e2638265
4 changed files with 139 additions and 2 deletions
4
go.mod
4
go.mod
|
|
@ -61,8 +61,8 @@ require (
|
|||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -183,9 +183,13 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
|
|
|||
93
pkg/ui/progress.go
Normal file
93
pkg/ui/progress.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// Progress abstracts output for both interactive and scripted use.
|
||||
type Progress interface {
|
||||
Start(label string)
|
||||
Update(current, total int64)
|
||||
Finish(label string)
|
||||
Log(level, msg string, args ...any)
|
||||
}
|
||||
|
||||
// QuietProgress writes structured log lines. For cron, pipes, --quiet.
|
||||
type QuietProgress struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func NewQuietProgress(w io.Writer) *QuietProgress {
|
||||
return &QuietProgress{w: w}
|
||||
}
|
||||
|
||||
func (q *QuietProgress) Start(label string) {
|
||||
fmt.Fprintf(q.w, "[START] %s\n", label)
|
||||
}
|
||||
|
||||
func (q *QuietProgress) Update(current, total int64) {
|
||||
if total > 0 {
|
||||
fmt.Fprintf(q.w, "[PROGRESS] %d/%d\n", current, total)
|
||||
}
|
||||
}
|
||||
|
||||
func (q *QuietProgress) Finish(label string) {
|
||||
fmt.Fprintf(q.w, "[DONE] %s\n", label)
|
||||
}
|
||||
|
||||
func (q *QuietProgress) Log(level, msg string, args ...any) {
|
||||
fmt.Fprintf(q.w, "[%s] %s", level, msg)
|
||||
for i := 0; i+1 < len(args); i += 2 {
|
||||
fmt.Fprintf(q.w, " %v=%v", args[i], args[i+1])
|
||||
}
|
||||
fmt.Fprintln(q.w)
|
||||
}
|
||||
|
||||
// InteractiveProgress uses simple terminal output for TTY sessions.
|
||||
type InteractiveProgress struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func NewInteractiveProgress(w io.Writer) *InteractiveProgress {
|
||||
return &InteractiveProgress{w: w}
|
||||
}
|
||||
|
||||
func (p *InteractiveProgress) Start(label string) {
|
||||
fmt.Fprintf(p.w, "→ %s\n", label)
|
||||
}
|
||||
|
||||
func (p *InteractiveProgress) Update(current, total int64) {
|
||||
if total > 0 {
|
||||
pct := current * 100 / total
|
||||
fmt.Fprintf(p.w, "\r %d%%", pct)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *InteractiveProgress) Finish(label string) {
|
||||
fmt.Fprintf(p.w, "\r✓ %s\n", label)
|
||||
}
|
||||
|
||||
func (p *InteractiveProgress) Log(level, msg string, args ...any) {
|
||||
fmt.Fprintf(p.w, " %s", msg)
|
||||
for i := 0; i+1 < len(args); i += 2 {
|
||||
fmt.Fprintf(p.w, " %v=%v", args[i], args[i+1])
|
||||
}
|
||||
fmt.Fprintln(p.w)
|
||||
}
|
||||
|
||||
// IsTTY returns true if the given file descriptor is a terminal.
|
||||
func IsTTY(fd int) bool {
|
||||
return term.IsTerminal(fd)
|
||||
}
|
||||
|
||||
// DefaultProgress returns InteractiveProgress for TTYs, QuietProgress otherwise.
|
||||
func DefaultProgress() Progress {
|
||||
if IsTTY(int(os.Stdout.Fd())) {
|
||||
return NewInteractiveProgress(os.Stdout)
|
||||
}
|
||||
return NewQuietProgress(os.Stdout)
|
||||
}
|
||||
40
pkg/ui/progress_test.go
Normal file
40
pkg/ui/progress_test.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestQuietProgress_Log_Good(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
p := NewQuietProgress(&buf)
|
||||
p.Log("info", "test message", "key", "val")
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "test message") {
|
||||
t.Fatalf("expected log output to contain 'test message', got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuietProgress_StartFinish_Good(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
p := NewQuietProgress(&buf)
|
||||
p.Start("collecting")
|
||||
p.Update(50, 100)
|
||||
p.Finish("done")
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "collecting") {
|
||||
t.Fatalf("expected 'collecting' in output, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "done") {
|
||||
t.Fatalf("expected 'done' in output, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuietProgress_Update_Ugly(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
p := NewQuietProgress(&buf)
|
||||
// Should not panic with zero total
|
||||
p.Update(0, 0)
|
||||
p.Update(5, 0)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue