diff --git a/bar.go b/bar.go index e336fa6..39607c3 100644 --- a/bar.go +++ b/bar.go @@ -87,7 +87,7 @@ } ) -func newBar(id int, total int64, width int, format string, wg *sync.WaitGroup, cancel <-chan struct{}) *Bar { +func newBar(id int, total int64, wg *sync.WaitGroup, conf *userConf) *Bar { b := &Bar{ stateReqCh: make(chan chan state), widthCh: make(chan int), @@ -103,7 +103,7 @@ completeReqCh: make(chan struct{}), done: make(chan struct{}), } - go b.server(id, total, width, format, wg, cancel) + go b.server(id, total, wg, conf) return b } @@ -205,8 +205,7 @@ // GetID returs id of the bar func (b *Bar) GetID() int { - state := b.getState() - return state.id + return b.getState().id } // InProgress returns true, while progress is running @@ -268,22 +267,21 @@ return <-ch } -func (b *Bar) server(id int, total int64, width int, format string, wg *sync.WaitGroup, cancel <-chan struct{}) { +func (b *Bar) server(id int, total int64, wg *sync.WaitGroup, conf *userConf) { var incrStartTime time.Time barState := state{ id: id, - width: width, - format: barFmtRunes{'[', '=', '>', '-', ']'}, + total: total, + width: conf.width, etaAlpha: 0.25, - total: total, } if total <= 0 { barState.simpleSpinner = getSpinner() } else { - barState.updateFormat(format) + barState.updateFormat(conf.format) } defer func() { - b.stop(&barState, width) + b.stop(&barState, conf.width) wg.Done() }() for { @@ -333,7 +331,7 @@ return case <-b.removeReqCh: return - case <-cancel: + case <-conf.cancel: return } } @@ -374,9 +372,6 @@ } func (s *state) updateFormat(format string) { - if format == "" { - return - } for i, n := 0, 0; len(format) > 0; i++ { s.format[i], n = utf8.DecodeRuneInString(format) format = format[n:] diff --git a/progress.go b/progress.go index d13d8ce..ba1c2af 100644 --- a/progress.go +++ b/progress.go @@ -21,6 +21,9 @@ // are called after (*Progress).Stop() has been called var ErrCallAfterStop = errors.New("method call on stopped Progress instance") +// default RefreshRate +var rr = 100 * time.Millisecond + type ( // BeforeRender is a func, which gets called before render process BeforeRender func([]*Bar) @@ -36,6 +39,18 @@ listen []chan int result []chan int } + + // config changeable by user + userConf struct { + width int + format string + beforeRender BeforeRender + cw *cwriter.Writer + ticker *time.Ticker + + shutdownNotifier chan struct{} + cancel <-chan struct{} + } ) const ( @@ -44,32 +59,24 @@ ) const ( - // default RefreshRate - rr = 100 // default width - pwidth = 70 + pwidth = 80 + // default format + pformat = "[=>-]" // number of format runes for bar numFmtRunes = 5 ) // Progress represents the container that renders Progress bars type Progress struct { - // Context for canceling bars rendering - // ctx context.Context // WaitGroup for internal rendering sync wg *sync.WaitGroup - width int - format string - - bCommandCh chan *bCommandData - rrChangeReqCh chan time.Duration - outChangeReqCh chan io.Writer - barCountReqCh chan chan int - brCh chan BeforeRender - done chan struct{} - beforeStop chan struct{} - cancel <-chan struct{} + done chan struct{} + userConf chan userConf + bCommandCh chan *bCommandData + barCountReqCh chan chan int + beforeStop chan struct{} } // New creates new Progress instance, which will orchestrate bars rendering @@ -77,43 +84,54 @@ // If you don't plan to cancel, it is safe to feed with nil func New() *Progress { p := &Progress{ - width: pwidth, - bCommandCh: make(chan *bCommandData), - rrChangeReqCh: make(chan time.Duration), - outChangeReqCh: make(chan io.Writer), - barCountReqCh: make(chan chan int), - brCh: make(chan BeforeRender), - done: make(chan struct{}), - beforeStop: make(chan struct{}), - wg: new(sync.WaitGroup), - } - go p.server() - return p -} - -// WithCancel cancellation via channel + wg: new(sync.WaitGroup), + done: make(chan struct{}), + userConf: make(chan userConf), + bCommandCh: make(chan *bCommandData), + barCountReqCh: make(chan chan int), + beforeStop: make(chan struct{}), + } + go p.server(userConf{ + width: pwidth, + format: pformat, + cw: cwriter.New(os.Stdout), + ticker: time.NewTicker(rr), + }) + return p +} + +// WithCancel cancellation via channel. +// pancis, if called on stopped Progress instance, i.e after (*Progress).Stop() +// or nil channel passed func (p *Progress) WithCancel(ch <-chan struct{}) *Progress { + if isClosed(p.done) { + panic(ErrCallAfterStop) + } if ch == nil { panic("nil cancel channel") } - p2 := new(Progress) - *p2 = *p - p2.cancel = ch - return p2 -} - -// SetWidth overrides default (70) width of bar(s) -func (p *Progress) SetWidth(n int) *Progress { - if n < 0 { - panic("negative width") - } - p2 := new(Progress) - *p2 = *p - p2.width = n - return p2 -} - -// SetOut sets underlying writer of progress. Default is os.Stdout + conf := <-p.userConf + conf.cancel = ch + p.userConf <- conf + return p +} + +// SetWidth overrides default (70) width of bar(s). +// pancis, if called on stopped Progress instance, i.e after (*Progress).Stop() +func (p *Progress) SetWidth(width int) *Progress { + if isClosed(p.done) { + panic(ErrCallAfterStop) + } + if width < 0 { + return p + } + conf := <-p.userConf + conf.width = width + p.userConf <- conf + return p +} + +// SetOut sets underlying writer of progress. Default one is os.Stdout. // pancis, if called on stopped Progress instance, i.e after (*Progress).Stop() func (p *Progress) SetOut(w io.Writer) *Progress { if isClosed(p.done) { @@ -122,7 +140,10 @@ if w == nil { return p } - p.outChangeReqCh <- w + conf := <-p.userConf + conf.cw.Flush() + conf.cw = cwriter.New(w) + p.userConf <- conf return p } @@ -132,33 +153,41 @@ if isClosed(p.done) { panic(ErrCallAfterStop) } - p.rrChangeReqCh <- d + conf := <-p.userConf + conf.ticker.Stop() + rr = d + conf.ticker = time.NewTicker(rr) + p.userConf <- conf return p } // BeforeRenderFunc accepts a func, which gets called before render process. +// pancis, if called on stopped Progress instance, i.e after (*Progress).Stop() func (p *Progress) BeforeRenderFunc(f BeforeRender) *Progress { if isClosed(p.done) { panic(ErrCallAfterStop) } - p.brCh <- f - return p -} - -// AddBar creates a new progress bar and adds to the container + conf := <-p.userConf + conf.beforeRender = f + p.userConf <- conf + return p +} + +// AddBar creates a new progress bar and adds to the container. // pancis, if called on stopped Progress instance, i.e after (*Progress).Stop() func (p *Progress) AddBar(total int64) *Bar { return p.AddBarWithID(0, total) } -// AddBarWithID creates a new progress bar and adds to the container +// AddBarWithID creates a new progress bar and adds to the container. // pancis, if called on stopped Progress instance, i.e after (*Progress).Stop() func (p *Progress) AddBarWithID(id int, total int64) *Bar { if isClosed(p.done) { panic(ErrCallAfterStop) } + conf := <-p.userConf result := make(chan bool) - bar := newBar(id, total, p.width, p.format, p.wg, p.cancel) + bar := newBar(id, total, p.wg, &conf) p.bCommandCh <- &bCommandData{bAdd, bar, result} if <-result { p.wg.Add(1) @@ -188,13 +217,30 @@ return <-respCh } -// Format sets custom format for underlying bar(s). -// The default one is "[=>-]" +// ShutdownNotify means to be notified when main rendering goroutine quits, usualy after p.Stop() call. +// pancis, if called on stopped Progress instance, i.e after (*Progress).Stop() +func (p *Progress) ShutdownNotify(ch chan struct{}) *Progress { + if isClosed(p.done) { + panic(ErrCallAfterStop) + } + conf := <-p.userConf + conf.shutdownNotifier = ch + p.userConf <- conf + return p +} + +// Format sets custom format for underlying bar(s), default one is "[=>-]". +// pancis, if called on stopped Progress instance, i.e after (*Progress).Stop() func (p *Progress) Format(format string) *Progress { + if isClosed(p.done) { + panic(ErrCallAfterStop) + } if utf8.RuneCountInString(format) != numFmtRunes { return p } - p.format = format + conf := <-p.userConf + conf.format = format + p.userConf <- conf return p } @@ -212,13 +258,14 @@ } // server monitors underlying channels and renders any progress bars -func (p *Progress) server() { - userRR := rr * time.Millisecond - t := time.NewTicker(userRR) +func (p *Progress) server(conf userConf) { defer func() { - t.Stop() + conf.ticker.Stop() close(p.done) + if conf.shutdownNotifier != nil { + close(conf.shutdownNotifier) + } }() recoverFn := func(ch chan []byte) { @@ -231,15 +278,12 @@ close(ch) } - var beforeRender BeforeRender - cw := cwriter.New(os.Stdout) bars := make([]*Bar, 0, 3) for { select { - case w := <-p.outChangeReqCh: - cw.Flush() - cw = cwriter.New(w) + case p.userConf <- conf: + case conf = <-p.userConf: case data, ok := <-p.bCommandCh: if !ok { return @@ -262,20 +306,19 @@ } case respCh := <-p.barCountReqCh: respCh <- len(bars) - case beforeRender = <-p.brCh: - case <-t.C: + case <-conf.ticker.C: numBars := len(bars) if numBars == 0 { break } - if beforeRender != nil { - beforeRender(bars) + if conf.beforeRender != nil { + conf.beforeRender(bars) } quitWidthSyncCh := make(chan struct{}) - time.AfterFunc(userRR, func() { + time.AfterFunc(rr, func() { close(quitWidthSyncCh) }) @@ -293,24 +336,21 @@ ch := fanIn(sequence...) for buf := range ch { - cw.Write(buf) - } - - cw.Flush() + conf.cw.Write(buf) + } + + conf.cw.Flush() for _, b := range bars { b.flushed() } - case userRR = <-p.rrChangeReqCh: - t.Stop() - t = time.NewTicker(userRR) case <-p.beforeStop: for _, b := range bars { if b.GetStatistics().Total <= 0 { b.Completed() } } - case <-p.cancel: + case <-conf.cancel: return } } diff --git a/progress_go1.7.go b/progress_go1.7.go index cc2690f..427a2ae 100644 --- a/progress_go1.7.go +++ b/progress_go1.7.go @@ -8,13 +8,18 @@ import "context" -// WithContext cancellation via context +// WithContext cancellation via context. +// pancis, if called on stopped Progress instance, i.e after (*Progress).Stop() +// or nil context passed func (p *Progress) WithContext(ctx context.Context) *Progress { + if isClosed(p.done) { + panic(ErrCallAfterStop) + } if ctx == nil { panic("nil context") } - p2 := new(Progress) - *p2 = *p - p2.cancel = ctx.Done() - return p2 + conf := <-p.userConf + conf.cancel = ctx.Done() + p.userConf <- conf + return p }