Use single progress bar when processing multiple paths

Instead of creating a new progress bar for each path, count total files
across all paths upfront and use a single unified progress bar. This
provides clearer UX when processing multiple directories.
This commit is contained in:
Jeffrey Paul 2026-02-02 13:46:18 -08:00
parent 144d2de243
commit b9d65115c2
2 changed files with 96 additions and 55 deletions

View File

@ -146,11 +146,21 @@ func newSumCmd() *cobra.Command {
return err return err
} }
stats := &Stats{StartTime: time.Now()} stats := &Stats{StartTime: time.Now()}
var bar *progressbar.ProgressBar
if !quiet {
bar = newProgressBar(countFilesMultiple(paths), "Adding checksums")
}
for _, p := range paths { for _, p := range paths {
if err := ProcessSumAdd(p, stats); err != nil { if err := ProcessSumAdd(p, stats, bar); err != nil {
if bar != nil {
bar.Finish()
}
return err return err
} }
} }
if bar != nil {
bar.Finish()
}
stats.Print("sum add") stats.Print("sum add")
return nil return nil
}, },
@ -166,11 +176,21 @@ func newSumCmd() *cobra.Command {
return err return err
} }
stats := &Stats{StartTime: time.Now()} stats := &Stats{StartTime: time.Now()}
var bar *progressbar.ProgressBar
if !quiet {
bar = newProgressBar(countFilesMultiple(paths), "Updating checksums")
}
for _, p := range paths { for _, p := range paths {
if err := ProcessSumUpdate(p, stats); err != nil { if err := ProcessSumUpdate(p, stats, bar); err != nil {
if bar != nil {
bar.Finish()
}
return err return err
} }
} }
if bar != nil {
bar.Finish()
}
stats.Print("sum update") stats.Print("sum update")
return nil return nil
}, },
@ -180,8 +200,8 @@ func newSumCmd() *cobra.Command {
return cmd return cmd
} }
func ProcessSumAdd(dir string, stats *Stats) error { func ProcessSumAdd(dir string, stats *Stats, bar *progressbar.ProgressBar) error {
return walkAndProcess(dir, stats, "Adding checksums", func(p string, info os.FileInfo, s *Stats) error { return walkAndProcess(dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error {
if hasXattr(p, checksumKey) { if hasXattr(p, checksumKey) {
atomic.AddInt64(&s.FilesSkipped, 1) atomic.AddInt64(&s.FilesSkipped, 1)
return nil return nil
@ -194,8 +214,8 @@ func ProcessSumAdd(dir string, stats *Stats) error {
}) })
} }
func ProcessSumUpdate(dir string, stats *Stats) error { func ProcessSumUpdate(dir string, stats *Stats, bar *progressbar.ProgressBar) error {
return walkAndProcess(dir, stats, "Updating checksums", func(p string, info os.FileInfo, s *Stats) error { return walkAndProcess(dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error {
t, err := readSumTime(p) t, err := readSumTime(p)
if err != nil || info.ModTime().After(t) { if err != nil || info.ModTime().After(t) {
if err := writeChecksumAndTime(p, info, s); err != nil { if err := writeChecksumAndTime(p, info, s); err != nil {
@ -257,19 +277,29 @@ func newClearCmd() *cobra.Command {
return err return err
} }
stats := &Stats{StartTime: time.Now()} stats := &Stats{StartTime: time.Now()}
var bar *progressbar.ProgressBar
if !quiet {
bar = newProgressBar(countFilesMultiple(paths), "Clearing checksums")
}
for _, p := range paths { for _, p := range paths {
if err := ProcessClear(p, stats); err != nil { if err := ProcessClear(p, stats, bar); err != nil {
if bar != nil {
bar.Finish()
}
return err return err
} }
} }
if bar != nil {
bar.Finish()
}
stats.Print("clear") stats.Print("clear")
return nil return nil
}, },
} }
} }
func ProcessClear(dir string, stats *Stats) error { func ProcessClear(dir string, stats *Stats, bar *progressbar.ProgressBar) error {
return walkAndProcess(dir, stats, "Clearing checksums", func(p string, info os.FileInfo, s *Stats) error { return walkAndProcess(dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error {
cleared := false cleared := false
for _, k := range []string{checksumKey, sumTimeKey} { for _, k := range []string{checksumKey, sumTimeKey} {
v, err := xattr.Get(p, k) v, err := xattr.Get(p, k)
@ -315,17 +345,27 @@ func newCheckCmd() *cobra.Command {
return err return err
} }
stats := &Stats{StartTime: time.Now()} stats := &Stats{StartTime: time.Now()}
var bar *progressbar.ProgressBar
if !quiet {
bar = newProgressBar(countFilesMultiple(paths), "Verifying checksums")
}
var finalErr error var finalErr error
for _, p := range paths { for _, p := range paths {
if err := ProcessCheck(p, cont, stats); err != nil { if err := ProcessCheck(p, cont, stats, bar); err != nil {
if cont { if cont {
finalErr = err finalErr = err
} else { } else {
if bar != nil {
bar.Finish()
}
stats.Print("check") stats.Print("check")
return err return err
} }
} }
} }
if bar != nil {
bar.Finish()
}
stats.Print("check") stats.Print("check")
return finalErr return finalErr
}, },
@ -334,11 +374,11 @@ func newCheckCmd() *cobra.Command {
return cmd return cmd
} }
func ProcessCheck(dir string, cont bool, stats *Stats) error { func ProcessCheck(dir string, cont bool, stats *Stats, bar *progressbar.ProgressBar) error {
fail := errors.New("verification failed") fail := errors.New("verification failed")
bad := false bad := false
err := walkAndProcess(dir, stats, "Verifying checksums", func(p string, info os.FileInfo, s *Stats) error { err := walkAndProcess(dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error {
exp, err := xattr.Get(p, checksumKey) exp, err := xattr.Get(p, checksumKey)
if err != nil { if err != nil {
if errors.Is(err, xattr.ENOATTR) { if errors.Is(err, xattr.ENOATTR) {
@ -429,34 +469,39 @@ func countFiles(root string) int64 {
return count return count
} }
func walkAndProcess(root string, stats *Stats, description string, fn func(string, os.FileInfo, *Stats) error) error { // countFilesMultiple counts files across multiple roots
root = filepath.Clean(root) func countFilesMultiple(roots []string) int64 {
var total int64
// Count files first for progress bar for _, root := range roots {
total := countFiles(root) total += countFiles(root)
// Create progress bar
var bar *progressbar.ProgressBar
if !quiet {
bar = progressbar.NewOptions64(total,
progressbar.OptionSetDescription(description),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionShowCount(),
progressbar.OptionShowIts(),
progressbar.OptionSetItsString("files"),
progressbar.OptionThrottle(250*time.Millisecond),
progressbar.OptionShowElapsedTimeOnFinish(),
progressbar.OptionSetPredictTime(true),
progressbar.OptionFullWidth(),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "=",
SaucerHead: ">",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
)
} }
return total
}
// newProgressBar creates a new progress bar with standard options
func newProgressBar(total int64, description string) *progressbar.ProgressBar {
return progressbar.NewOptions64(total,
progressbar.OptionSetDescription(description),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionShowCount(),
progressbar.OptionShowIts(),
progressbar.OptionSetItsString("files"),
progressbar.OptionThrottle(250*time.Millisecond),
progressbar.OptionShowElapsedTimeOnFinish(),
progressbar.OptionSetPredictTime(true),
progressbar.OptionFullWidth(),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "=",
SaucerHead: ">",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
)
}
func walkAndProcess(root string, stats *Stats, bar *progressbar.ProgressBar, fn func(string, os.FileInfo, *Stats) error) error {
root = filepath.Clean(root)
err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error { err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
@ -497,10 +542,6 @@ func walkAndProcess(root string, stats *Stats, description string, fn func(strin
return fnErr return fnErr
}) })
if bar != nil {
bar.Finish()
}
return err return err
} }

View File

@ -40,7 +40,7 @@ func TestSumAddAndUpdate(t *testing.T) {
f := writeFile(t, dir, "a.txt", "hello") f := writeFile(t, dir, "a.txt", "hello")
if err := ProcessSumAdd(dir, newTestStats()); err != nil { if err := ProcessSumAdd(dir, newTestStats(), nil); err != nil {
t.Fatalf("add: %v", err) t.Fatalf("add: %v", err)
} }
if _, err := xattr.Get(f, checksumKey); err != nil { if _, err := xattr.Get(f, checksumKey); err != nil {
@ -53,7 +53,7 @@ func TestSumAddAndUpdate(t *testing.T) {
now := time.Now().Add(2 * time.Second) now := time.Now().Add(2 * time.Second)
os.Chtimes(f, now, now) os.Chtimes(f, now, now)
if err := ProcessSumUpdate(dir, newTestStats()); err != nil { if err := ProcessSumUpdate(dir, newTestStats(), nil); err != nil {
t.Fatalf("update: %v", err) t.Fatalf("update: %v", err)
} }
tsb2, _ := xattr.Get(f, sumTimeKey) tsb2, _ := xattr.Get(f, sumTimeKey)
@ -68,17 +68,17 @@ func TestProcessCheckIntegration(t *testing.T) {
skipIfNoXattr(t, dir) skipIfNoXattr(t, dir)
writeFile(t, dir, "b.txt", "world") writeFile(t, dir, "b.txt", "world")
if err := ProcessSumAdd(dir, newTestStats()); err != nil { if err := ProcessSumAdd(dir, newTestStats(), nil); err != nil {
t.Fatalf("add: %v", err) t.Fatalf("add: %v", err)
} }
if err := ProcessCheck(dir, false, newTestStats()); err != nil { if err := ProcessCheck(dir, false, newTestStats(), nil); err != nil {
t.Fatalf("check ok: %v", err) t.Fatalf("check ok: %v", err)
} }
f := filepath.Join(dir, "b.txt") f := filepath.Join(dir, "b.txt")
os.WriteFile(f, []byte("corrupt"), 0o644) os.WriteFile(f, []byte("corrupt"), 0o644)
if err := ProcessCheck(dir, false, newTestStats()); err == nil { if err := ProcessCheck(dir, false, newTestStats(), nil); err == nil {
t.Fatalf("expected mismatch error, got nil") t.Fatalf("expected mismatch error, got nil")
} }
} }
@ -88,11 +88,11 @@ func TestClearRemovesAttrs(t *testing.T) {
skipIfNoXattr(t, dir) skipIfNoXattr(t, dir)
f := writeFile(t, dir, "c.txt", "data") f := writeFile(t, dir, "c.txt", "data")
if err := ProcessSumAdd(dir, newTestStats()); err != nil { if err := ProcessSumAdd(dir, newTestStats(), nil); err != nil {
t.Fatalf("add: %v", err) t.Fatalf("add: %v", err)
} }
if err := ProcessClear(dir, newTestStats()); err != nil { if err := ProcessClear(dir, newTestStats(), nil); err != nil {
t.Fatalf("clear: %v", err) t.Fatalf("clear: %v", err)
} }
if _, err := xattr.Get(f, checksumKey); err == nil { if _, err := xattr.Get(f, checksumKey); err == nil {
@ -117,7 +117,7 @@ func TestExcludeDotfilesAndPatterns(t *testing.T) {
excludePatterns = []string{"*.me"} excludePatterns = []string{"*.me"}
defer func() { excludeDotfiles, excludePatterns = oldDot, oldPat }() defer func() { excludeDotfiles, excludePatterns = oldDot, oldPat }()
if err := ProcessSumAdd(dir, newTestStats()); err != nil { if err := ProcessSumAdd(dir, newTestStats(), nil); err != nil {
t.Fatalf("add with excludes: %v", err) t.Fatalf("add with excludes: %v", err)
} }
@ -143,7 +143,7 @@ func TestSkipBrokenSymlink(t *testing.T) {
} }
// Should not error and should not create xattrs on link // Should not error and should not create xattrs on link
if err := ProcessSumAdd(dir, newTestStats()); err != nil { if err := ProcessSumAdd(dir, newTestStats(), nil); err != nil {
t.Fatalf("ProcessSumAdd with symlink: %v", err) t.Fatalf("ProcessSumAdd with symlink: %v", err)
} }
if _, err := xattr.Get(link, checksumKey); err == nil { if _, err := xattr.Get(link, checksumKey); err == nil {
@ -159,13 +159,13 @@ func TestPermissionErrors(t *testing.T) {
os.Chmod(secret, 0o000) os.Chmod(secret, 0o000)
defer os.Chmod(secret, 0o644) defer os.Chmod(secret, 0o644)
if err := ProcessSumAdd(dir, newTestStats()); err == nil { if err := ProcessSumAdd(dir, newTestStats(), nil); err == nil {
t.Fatalf("expected permission error, got nil") t.Fatalf("expected permission error, got nil")
} }
if err := ProcessSumUpdate(dir, newTestStats()); err == nil { if err := ProcessSumUpdate(dir, newTestStats(), nil); err == nil {
t.Fatalf("expected permission error on update, got nil") t.Fatalf("expected permission error on update, got nil")
} }
if err := ProcessCheck(dir, false, newTestStats()); err == nil { if err := ProcessCheck(dir, false, newTestStats(), nil); err == nil {
t.Fatalf("expected permission error on check, got nil") t.Fatalf("expected permission error on check, got nil")
} }
} }