From 355b99d3054e86fbe47b3064b4ad736faac12e70 Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Thu, 29 Oct 2026 10:43:26 +0100 Subject: [PATCH] Add in place progress print --- cmd/catp/README.md | 1 + cmd/catp/catp/app.go | 4 +++- cmd/catp/catp/catp.go | 5 +++-- progress.go | 31 ++++++++++++++++++++++++++++++- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/cmd/catp/README.md b/cmd/catp/README.md index 3275429..32ac3e5 100644 --- a/cmd/catp/README.md +++ b/cmd/catp/README.md @@ -38,6 +38,7 @@ catp [OPTIONS] PATH ... POST /rate-limit?value=X sets a new one (0 or less means unlimited) -http-auth string user:pass to require as HTTP basic auth for -http endpoints + -i rewrite progress line in place (terminal only) -l count lines -no-progress disable progress printing diff --git a/cmd/catp/catp/app.go b/cmd/catp/catp/app.go index 8f2a5cc..dcc4f09 100644 --- a/cmd/catp/catp/app.go +++ b/cmd/catp/catp/app.go @@ -95,6 +95,7 @@ func Main(options ...func(o *Options)) error { //nolint:funlen,cyclop,gocognit,g cpuProfile := flag.String("dbg-cpu-prof", "", "write first 10 seconds of CPU profile to file") memProfile := flag.String("dbg-mem-prof", "", "write heap profile to file after 10 seconds") output := flag.String("output", "", "output to file (can have .gz or .zst ext for compression) instead of STDOUT") + flag.BoolVar(&r.progressInPlace, "i", false, "rewrite progress line in place (terminal only)") flag.BoolVar(&r.noProgress, "no-progress", false, "disable progress printing") flag.BoolVar(&r.countLines, "l", false, "count lines") flag.Float64Var(&r.rateLimit, "rate-limit", 0, "output rate limit lines per second") @@ -228,9 +229,10 @@ func Main(options ...func(o *Options)) error { //nolint:funlen,cyclop,gocognit,g return } - println(s) + r.pr.Println(s) }, } + r.pr.InPlace = r.progressInPlace for _, fn := range files { if fn == "-" { diff --git a/cmd/catp/catp/catp.go b/cmd/catp/catp/catp.go index b899a28..e2686a0 100644 --- a/cmd/catp/catp/catp.go +++ b/cmd/catp/catp/catp.go @@ -78,8 +78,9 @@ type runner struct { lastStatus string speed []speedSample // Kept only with -http, for the status page charts. - noProgress bool - countLines bool + noProgress bool + progressInPlace bool + countLines bool startLine int endLine int diff --git a/progress.go b/progress.go index 4654e14..2c4f5cf 100644 --- a/progress.go +++ b/progress.go @@ -4,6 +4,7 @@ package progress import ( "fmt" "io" + "os" "runtime" "sync" "sync/atomic" @@ -35,6 +36,9 @@ type Progress struct { // IncrementalSpeed shows speed and remaining estimate based on performance between two status updates. IncrementalSpeed bool + // InPlace rewrites the status line in place instead of appending lines, only effective if stderr is a terminal. + InPlace bool + done chan bool task Task lines func() int64 @@ -150,7 +154,7 @@ func (p *Progress) Start(options ...func(t *Task)) { p.prnt = p.Print if p.prnt == nil { p.prnt = func(s Status) { - println(DefaultStatus(s)) + p.Println(DefaultStatus(s)) } } @@ -266,10 +270,35 @@ func (p *Progress) printStatus(last bool) { } } +// Println prints a status line to stderr, honoring InPlace. +func (p *Progress) Println(s string) { + if p.inPlace() { + print("\r" + s + "\033[K") // \033[K erases leftovers of a longer previous line. + + return + } + + println(s) +} + +func (p *Progress) inPlace() bool { + if !p.InPlace { + return false + } + + st, err := os.Stderr.Stat() + + return err == nil && st.Mode()&os.ModeCharDevice != 0 +} + // Stop stops progress reporting. func (p *Progress) Stop() { p.printStatus(true) + if p.inPlace() { + println() // Keep the final status and move to a fresh line. + } + if !p.task.Continue { p.metrics = nil }