diff --git a/cmd/streamdata/progress.go b/cmd/streamdata/progress.go new file mode 100644 index 0000000..16f6d90 --- /dev/null +++ b/cmd/streamdata/progress.go @@ -0,0 +1,90 @@ +package main + +import ( + "io" + "os" + "strconv" + "sync/atomic" + "syscall" + "time" + + "hakurei.app/ext" +) + +// pendingMessage is written to the current line every tick if non-nil. +var pendingMessage atomic.Pointer[string] + +func init() { + ticker := time.NewTicker(time.Second / 100) + go func() { + for { + <-ticker.C + + p := pendingMessage.Swap(nil) + if p == nil { + continue + } + os.Stderr.WriteString("\x1b[2K" + *p + "\r") + } + }() +} + +// progressWriter writes a progress bar via writeCurrent. +type progressWriter struct { + // For progress indication. Has no effect unless greater than zero. + contentLength int64 + // Accumulated bytes. + n int64 + + // Increments with n, but resets periodically. + segment int64 + // Reference time since segment was reset. + ref time.Time +} + +// measureInterval is the interval at which progressWriter.segment is reset. +const measureInterval = 5 * time.Second + +func (pw *progressWriter) Write(p []byte) (n int, err error) { + n = len(p) + pw.n += int64(n) + pw.segment += int64(n) + d := time.Now().Sub(pw.ref) + + var s string + if pw.contentLength > 0 { + s += " " + strconv.FormatInt(pw.n/(1<<20), 10) + " / " + + strconv.FormatInt(pw.contentLength/(1<<20), 10) + " MiB |" + } + s += " " + strconv.FormatFloat( + float64(pw.segment)/d.Seconds()/float64(1<<20), + 'f', 2, 64, + ) + " MiB/s" + pendingMessage.Store(&s) + + if d > measureInterval { + pw.segment = 0 + pw.ref = time.Now() + } + + return +} + +// copyProgress is like [io.Copy], but prints progress to [os.Stderr] if it is +// attached to a terminal. +// +// Percentage is emitted if contentLength is greater than zero. +func copyProgress(dst io.Writer, src io.Reader, contentLength int64) ( + written int64, + err error, +) { + if !ext.Isatty(syscall.Stderr) { + return io.Copy(dst, src) + } + defer os.Stderr.WriteString("\n") + return io.Copy(io.MultiWriter(dst, &progressWriter{ + contentLength: contentLength, + + ref: time.Now(), + }), src) +} diff --git a/go.mod b/go.mod index 055db0f..06781e3 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module git.gensokyo.uk/yonah/streamdata -go 1.25.7 +go 1.26 + +require hakurei.app v0.3.8-0.20260317115839-6cdb6a652b63 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5de2cc6 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +hakurei.app v0.3.8-0.20260317115839-6cdb6a652b63 h1:Nx5eaHEbWUx6pwA4A+EdmHXvCvO3Xbw7PPcT1QbsVcA= +hakurei.app v0.3.8-0.20260317115839-6cdb6a652b63/go.mod h1:rIWpoHYiZtIPue49bQLkROpQHYh/j2VjJGFrzNrvixU=