Skip to content

Commit

Permalink
feat: Multiple progressbars and spinners support (#544)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarvinJWendt committed Aug 20, 2023
1 parent 3ffa822 commit a6d54ab
Show file tree
Hide file tree
Showing 12 changed files with 302 additions and 35 deletions.
53 changes: 53 additions & 0 deletions _examples/multiple-live-printers/demo/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"github.com/pterm/pterm"
"time"
)

func main() {
multi := pterm.DefaultMultiPrinter

spinner1, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Spinner 1")
spinner2, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Spinner 2")
pb1, _ := pterm.DefaultProgressbar.WithTotal(100).WithWriter(multi.NewWriter()).Start("Progressbar 1")
pb2, _ := pterm.DefaultProgressbar.WithTotal(100).WithWriter(multi.NewWriter()).Start("Progressbar 2")
pb3, _ := pterm.DefaultProgressbar.WithTotal(100).WithWriter(multi.NewWriter()).Start("Progressbar 3")
pb4, _ := pterm.DefaultProgressbar.WithTotal(100).WithWriter(multi.NewWriter()).Start("Progressbar 4")
pb5, _ := pterm.DefaultProgressbar.WithTotal(100).WithWriter(multi.NewWriter()).Start("Progressbar 5")

multi.Start()

// Randomly increment progress bars for demo purposes.
for i := 1; i <= 100; i++ {
pb1.Increment()

if i%2 == 0 {
pb2.Add(3)
}

if i%5 == 0 {
pb3.Increment()
}

if i%10 == 0 {
pb4.Increment()
}

if i%3 == 0 {
pb5.Increment()
}

if i%50 == 0 {
spinner1.Success("Spinner 1 is done!")
}

if i%60 == 0 {
spinner2.Fail("Spinner 2 failed!")
}

time.Sleep(time.Millisecond * 50)
}

multi.Stop()
}
43 changes: 43 additions & 0 deletions _examples/progressbar/multiple/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"github.com/pterm/pterm"
"time"
)

func main() {
multi := pterm.DefaultMultiPrinter

pb1, _ := pterm.DefaultProgressbar.WithTotal(100).WithWriter(multi.NewWriter()).Start("Progressbar 1")
pb2, _ := pterm.DefaultProgressbar.WithTotal(100).WithWriter(multi.NewWriter()).Start("Progressbar 2")
pb3, _ := pterm.DefaultProgressbar.WithTotal(100).WithWriter(multi.NewWriter()).Start("Progressbar 3")
pb4, _ := pterm.DefaultProgressbar.WithTotal(100).WithWriter(multi.NewWriter()).Start("Progressbar 4")
pb5, _ := pterm.DefaultProgressbar.WithTotal(100).WithWriter(multi.NewWriter()).Start("Progressbar 5")

multi.Start()

// Randomly increment progress bars for demo purposes.
for i := 1; i <= 100; i++ {
pb1.Increment()

if i%2 == 0 {
pb2.Add(3)
}

if i%5 == 0 {
pb3.Increment()
}

if i%10 == 0 {
pb4.Increment()
}

if i%3 == 0 {
pb5.Increment()
}

time.Sleep(time.Millisecond * 50)
}

multi.Stop()
}
25 changes: 25 additions & 0 deletions _examples/spinner/multiple/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"github.com/pterm/pterm"
"time"
)

func main() {
multi := pterm.DefaultMultiPrinter

spinner1, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Spinner 1")
spinner2, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Spinner 2")
spinner3, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Spinner 3")

multi.Start()

time.Sleep(time.Millisecond * 1000)
spinner1.Success("Spinner 1 is done!")
time.Sleep(time.Millisecond * 750)
spinner2.Fail("Spinner 2 failed!")
time.Sleep(time.Millisecond * 500)
spinner3.Warning("Spinner 3 has a warning!")

multi.Stop()
}
6 changes: 6 additions & 0 deletions area_printer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package pterm

import (
"io"
"strings"

"atomicgo.dev/cursor"
Expand Down Expand Up @@ -47,6 +48,11 @@ func (p AreaPrinter) WithCenter(b ...bool) *AreaPrinter {
return &p
}

// SetWriter sets the writer for the AreaPrinter.
func (p *AreaPrinter) SetWriter(writer io.Writer) {

}

// Update overwrites the content of the AreaPrinter.
// Can be used live.
func (p *AreaPrinter) Update(text ...interface{}) {
Expand Down
2 changes: 1 addition & 1 deletion ci/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func main() {
os.WriteFile("./docs/docs/putils.md", []byte(putilsReadme), 0600)
})

do("Geneating Printers Table", currentLevel, func(currentLevel int) {
do("Geneating printers Table", currentLevel, func(currentLevel int) {
// get features located in "_examples/*"
files, _ := os.ReadDir("./_examples/")

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.18
require (
atomicgo.dev/cursor v0.2.0
atomicgo.dev/keyboard v0.2.9
atomicgo.dev/schedule v0.0.2
atomicgo.dev/schedule v0.1.0
github.com/MarvinJWendt/testza v0.5.2
github.com/gookit/color v1.5.4
github.com/lithammer/fuzzysearch v1.1.8
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
atomicgo.dev/schedule v0.0.2 h1:2e/4KY6t3wokja01Cyty6qgkQM8MotJzjtqCH70oX2Q=
atomicgo.dev/schedule v0.0.2/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
Expand Down
4 changes: 4 additions & 0 deletions interface_live_printer.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package pterm

import "io"

// LivePrinter is a printer which can update it's output live.
type LivePrinter interface {
// GenericStart runs Start, but returns a LivePrinter.
Expand All @@ -11,4 +13,6 @@ type LivePrinter interface {
// This is used for the interface LivePrinter.
// You most likely want to use Stop instead of this in your program.
GenericStop() (*LivePrinter, error)

SetWriter(writer io.Writer)
}
124 changes: 124 additions & 0 deletions multi_live_printer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package pterm

import (
"atomicgo.dev/schedule"
"bytes"
"io"
"os"
"strings"
"time"
)

var DefaultMultiPrinter = MultiPrinter{
printers: []LivePrinter{},
Writer: os.Stdout,
UpdateDelay: time.Millisecond * 200,

buffers: []*bytes.Buffer{},
area: DefaultArea,
}

type MultiPrinter struct {
IsActive bool
Writer io.Writer
UpdateDelay time.Duration

printers []LivePrinter
buffers []*bytes.Buffer
area AreaPrinter
}

// SetWriter sets the writer for the AreaPrinter.
func (p *MultiPrinter) SetWriter(writer io.Writer) {
p.Writer = writer
}

// WithWriter returns a fork of the MultiPrinter with a new writer.
func (p MultiPrinter) WithWriter(writer io.Writer) *MultiPrinter {
p.Writer = writer
return &p
}

// WithUpdateDelay returns a fork of the MultiPrinter with a new update delay.
func (p MultiPrinter) WithUpdateDelay(delay time.Duration) *MultiPrinter {
p.UpdateDelay = delay
return &p
}

func (p *MultiPrinter) NewWriter() io.Writer {
buf := bytes.NewBufferString("")
p.buffers = append(p.buffers, buf)
return buf
}

// getString returns all buffers appended and separated by a newline.
func (p *MultiPrinter) getString() string {
var buffer bytes.Buffer
for _, b := range p.buffers {
s := b.String()
s = strings.Trim(s, "\n")

parts := strings.Split(s, "\r") // only get the last override
s = parts[len(parts)-1]

// check if s is empty, if so get one part before, repeat until not empty
for s == "" {
parts = parts[:len(parts)-1]
s = parts[len(parts)-1]
}

s = strings.Trim(s, "\n\r")
buffer.WriteString(s)
buffer.WriteString("\n")
}
return buffer.String()
}

func (p *MultiPrinter) Start() (*MultiPrinter, error) {
p.IsActive = true
for _, printer := range p.printers {
printer.GenericStart()
}

schedule.Every(p.UpdateDelay, func() bool {
if !p.IsActive {
return false
}

p.area.Update(p.getString())

return true
})

return p, nil
}

func (p *MultiPrinter) Stop() (*MultiPrinter, error) {
p.IsActive = false
for _, printer := range p.printers {
printer.GenericStop()
}
time.Sleep(time.Millisecond * 20)
p.area.Update(p.getString())
p.area.Stop()

return p, nil
}

// GenericStart runs Start, but returns a LivePrinter.
// This is used for the interface LivePrinter.
// You most likely want to use Start instead of this in your program.
func (p MultiPrinter) GenericStart() (*LivePrinter, error) {
p2, _ := p.Start()
lp := LivePrinter(p2)
return &lp, nil
}

// GenericStop runs Stop, but returns a LivePrinter.
// This is used for the interface LivePrinter.
// You most likely want to use Stop instead of this in your program.
func (p MultiPrinter) GenericStop() (*LivePrinter, error) {
p2, _ := p.Stop()
lp := LivePrinter(p2)
return &lp, nil
}

0 comments on commit a6d54ab

Please sign in to comment.