Skip to content

Commit

Permalink
Implement multi-spinner. Non-interactive has unpolished UI/UX at this…
Browse files Browse the repository at this point in the history
… stage.
  • Loading branch information
Naatan committed Aug 21, 2024
1 parent d9d6352 commit c8ce0a5
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 5 deletions.
2 changes: 1 addition & 1 deletion internal/output/progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (d *Spinner) MarshalOutput(f Format) interface{} {
func StartSpinner(out Outputer, msg string, interval time.Duration) *Spinner {
frames := []string{".", "..", "..."}
if out.Config().Interactive {
frames = []string{`|`, `/`, `-`, `\`}
frames = SpinnerFrames
}
d := &Spinner{0, frames, out, make(chan struct{}, 1), interval, false}

Expand Down
49 changes: 49 additions & 0 deletions internal/output/spinner/noninteractive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package spinner

import (
"os"
"time"

"github.com/ActiveState/cli/internal/constants"
)

type nonInteractive struct {
isGroup bool
supportsColors bool
stop chan struct{}
}

func newNonInteractive(isGroup, supportColors bool) *nonInteractive {
n := &nonInteractive{isGroup: isGroup, supportsColors: supportColors, stop: make(chan struct{}, 1)}
go n.ticker()
return n
}

func (n *nonInteractive) ticker() {
ticker := time.NewTicker(constants.TerminalAnimationInterval)
for {
select {
case <-ticker.C:
os.Stderr.WriteString(".")
case <-n.stop:
return
}
}
}

func (n *nonInteractive) Add(prefix string) Spinnable {
os.Stderr.WriteString("\n" + color(prefix, !n.supportsColors) + " ")
return n
}

func (n *nonInteractive) Wait() {
n.stop <- struct{}{}
}

func (n *nonInteractive) Stop(msg string) {
os.Stderr.WriteString(color(msg, !n.supportsColors) + "\n")
if !n.isGroup {
// If this isn't a group Wait will never be called
n.stop <- struct{}{}
}
}
124 changes: 124 additions & 0 deletions internal/output/spinner/spinner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package spinner

import (
"bytes"
"io"
"strings"

"github.com/ActiveState/cli/internal/colorize"
"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/logging"
"github.com/vbauerster/mpb/v7"
"github.com/vbauerster/mpb/v7/decor"
)

type Groupable interface {
Add(prefix string) Spinnable
Wait()
}

type Spinnable interface {
Stop(msg string)
}

// Group collects multiple spinners
type Group struct {
mpbGroup *mpb.Progress
supportColors bool
interactive bool
}

// Spinner represents a single spinner
type Spinner struct {
mpbBar *mpb.Bar
completionMsg string
}

// StandaloneSpinner represents a single spinner that is used in standalone, meaning it doesn't have a group.
type StandaloneSpinner struct {
*Spinner
mpbGroup *mpb.Progress
}

func NewGroup(supportColors, interactive bool) Groupable {
if !interactive {
return newNonInteractive(true, supportColors)
}
return &Group{mpb.New(
mpb.WithWidth(1)),
supportColors,
interactive,
}
}

func NewSpinner(prefix string, supportColors, interactive bool) Spinnable {
if !interactive {
n := newNonInteractive(false, supportColors)
n.Add(prefix)
return n
}
mpbGroup := mpb.New(mpb.WithWidth(1))
return &StandaloneSpinner{newSpinner(mpbGroup, prefix, supportColors, interactive), mpbGroup}
}

func newSpinner(mpbGroup *mpb.Progress, prefix string, supportColors, interactive bool) *Spinner {
s := &Spinner{}
p := mpbGroup.Add(
1,
mpb.NewBarFiller(mpb.SpinnerStyle([]string{`|`, `/`, `-`, `\`}...)),
mpb.PrependDecorators(decor.Any(func(s decor.Statistics) string {
return color(prefix, !supportColors)
})),
barFillerOnComplete(func() string { return color(strings.TrimPrefix(s.completionMsg, " "), !supportColors) }),
)
s.mpbBar = p
return s
}

func (g *Group) Add(prefix string) Spinnable {
s := newSpinner(g.mpbGroup, prefix, g.supportColors, g.interactive)
return s
}

func (s *Spinner) Stop(msg string) {
s.completionMsg = msg
s.mpbBar.Increment() // Our "bar" has a total of 1, so a single increment will complete it
}

func (s *StandaloneSpinner) Stop(msg string) {
s.Spinner.Stop(msg)
s.mpbGroup.Wait()
}

func (g *Group) Wait() {
g.mpbGroup.Wait()
}

func color(v string, strip bool) string {
if strip {
return colorize.StripColorCodes(v)
}

b := &bytes.Buffer{}
_, err := colorize.Colorize(v, b, false)
if err != nil {
logging.Warning("colorize failed, stripping colors - error: %s", errs.JoinMessage(err))
v = colorize.StripColorCodes(v)
} else {
v = b.String()
}

return v
}

func barFillerOnComplete(value func() string) mpb.BarOption {
return mpb.BarFillerMiddleware(func(base mpb.BarFiller) mpb.BarFiller {
return mpb.BarFillerFunc(func(w io.Writer, reqWidth int, st decor.Statistics) {
if st.Completed {
io.WriteString(w, value())
} else {
base.Fill(w, reqWidth, st)
}
})
})
}
17 changes: 13 additions & 4 deletions internal/runbits/runtime/requirements/requirements.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/ActiveState/cli/internal/logging"
"github.com/ActiveState/cli/internal/multilog"
"github.com/ActiveState/cli/internal/output"
"github.com/ActiveState/cli/internal/output/spinner"
"github.com/ActiveState/cli/internal/primer"
"github.com/ActiveState/cli/internal/prompt"
"github.com/ActiveState/cli/internal/rtutils/ptr"
Expand Down Expand Up @@ -137,13 +138,18 @@ func (r *RequirementOperation) ExecuteRequirementOperation(ts *time.Time, requir
return errNoRequirements
}

pgGroup := spinner.NewGroup(r.Output.Config().Colored, r.Output.Config().Interactive)

out := r.Output
var pg *output.Spinner
var pg spinner.Spinnable
defer func() {
// This is a bit awkward, but it would be even more awkward to manually address this for every error condition
if pg != nil {
// This is a bit awkward, but it would be even more awkward to manually address this for every error condition
pg.Stop(locale.T("progress_fail"))
}
if pgGroup != nil {
pgGroup.Wait()
}
}()

if r.Project == nil {
Expand All @@ -168,7 +174,7 @@ func (r *RequirementOperation) ExecuteRequirementOperation(ts *time.Time, requir
}
hasParentCommit := parentCommitID != ""

pg = output.StartSpinner(out, locale.T("progress_commit"), constants.TerminalAnimationInterval)
pg = pgGroup.Add(locale.T("progress_commit"))

if err := r.checkForUpdate(parentCommitID, requirements...); err != nil {
return locale.WrapError(err, "err_check_for_update", "Could not check for requirements updates")
Expand Down Expand Up @@ -214,7 +220,7 @@ func (r *RequirementOperation) ExecuteRequirementOperation(ts *time.Time, requir
}

// Solve runtime
solveSpinner := output.StartSpinner(r.Output, locale.T("progress_solve_preruntime"), constants.TerminalAnimationInterval)
solveSpinner := pgGroup.Add(locale.T("progress_solve_preruntime"))
commit, err := bp.StageCommit(params)
if err != nil {
solveSpinner.Stop(locale.T("progress_fail"))
Expand Down Expand Up @@ -242,6 +248,9 @@ func (r *RequirementOperation) ExecuteRequirementOperation(ts *time.Time, requir
}
solveSpinner.Stop(locale.T("progress_success"))

pgGroup.Wait()
pgGroup = nil

dependencies.OutputChangeSummary(r.prime.Output(), commit.BuildPlan(), oldCommit.BuildPlan())

// Report CVEs
Expand Down

0 comments on commit c8ce0a5

Please sign in to comment.