From cf1ca5954f6f1a8b3e1daa6bc758337ac093ed7b Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 8 Dec 2020 17:35:49 -0800 Subject: [PATCH] campaigns: add -vv to show executed commands --- CHANGELOG.md | 2 + cmd/src/campaigns_apply.go | 6 + cmd/src/campaigns_common.go | 13 ++ cmd/src/campaigns_preview.go | 6 + internal/campaigns/action.go | 2 +- internal/campaigns/command.go | 18 ++ internal/campaigns/run_steps.go | 8 +- internal/campaigns/service.go | 24 +++ internal/output/_examples/main.go | 159 ++++++++++++++---- internal/output/output.go | 9 + internal/output/progress_simple.go | 10 +- internal/output/progress_tty.go | 33 ++-- .../progress_with_status_bars_simple.go | 2 +- .../output/progress_with_status_bars_tty.go | 52 +++--- 14 files changed, 252 insertions(+), 92 deletions(-) create mode 100644 internal/campaigns/command.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b92a3984e..ddf84027b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ All notable changes to `src-cli` are documented in this file. ### Added +- `src campaign [apply|preview]` now accept a `-vv` option to display even more verbose output, most notably every command being spawned during execution. [#402](https://github.com/sourcegraph/src-cli/pull/402) + ### Changed - `src campaign [apply|preview]` now show the current execution progress in numbers next to the progress bar. [#396](https://github.com/sourcegraph/src-cli/pull/396) diff --git a/cmd/src/campaigns_apply.go b/cmd/src/campaigns_apply.go index 958550ed67..91ebc05c30 100644 --- a/cmd/src/campaigns_apply.go +++ b/cmd/src/campaigns_apply.go @@ -62,6 +62,11 @@ Examples: return &usageError{errors.New("additional arguments not allowed")} } + // -vv implies -v. + if flags.superVerbose { + *verbose = true + } + out := output.NewOutput(flagSet.Output(), output.OutputOpts{Verbose: *verbose}) ctx, cancel := contextCancelOnInterrupt(context.Background()) @@ -70,6 +75,7 @@ Examples: svc := campaigns.NewService(&campaigns.ServiceOpts{ AllowUnsupported: flags.allowUnsupported, Client: cfg.apiClient(flags.api, flagSet.Output()), + OnCommand: campaignsOnCommand(out, flags.superVerbose), }) if err := svc.DetermineFeatureFlags(ctx); err != nil { diff --git a/cmd/src/campaigns_common.go b/cmd/src/campaigns_common.go index 4c006b9050..572efbc99c 100644 --- a/cmd/src/campaigns_common.go +++ b/cmd/src/campaigns_common.go @@ -39,6 +39,7 @@ type campaignsApplyFlags struct { keepLogs bool namespace string parallelism int + superVerbose bool timeout time.Duration cleanArchives bool skipErrors bool @@ -102,6 +103,8 @@ func newCampaignsApplyFlags(flagSet *flag.FlagSet, cacheDir, tempDir string) *ca flagSet.BoolVar(verbose, "v", false, "print verbose output") + flagSet.BoolVar(&caf.superVerbose, "vv", false, "print more verbose output") + return caf } @@ -463,3 +466,13 @@ func contextCancelOnInterrupt(parent context.Context) (context.Context, func()) ctxCancel() } } + +func campaignsOnCommand(out *output.Output, superVerbose bool) campaigns.OnCommand { + if superVerbose { + return func(cmd *exec.Cmd) { + out.VerboseLine(output.Line("🚀", output.Fg256Color(184), cmd.String())) + } + } + + return nil +} diff --git a/cmd/src/campaigns_preview.go b/cmd/src/campaigns_preview.go index 83ce4b98f3..154d418858 100644 --- a/cmd/src/campaigns_preview.go +++ b/cmd/src/campaigns_preview.go @@ -37,6 +37,11 @@ Examples: return &usageError{errors.New("additional arguments not allowed")} } + // -vv implies -v. + if flags.superVerbose { + *verbose = true + } + out := output.NewOutput(flagSet.Output(), output.OutputOpts{Verbose: *verbose}) ctx, cancel := contextCancelOnInterrupt(context.Background()) @@ -45,6 +50,7 @@ Examples: svc := campaigns.NewService(&campaigns.ServiceOpts{ AllowUnsupported: flags.allowUnsupported, Client: cfg.apiClient(flags.api, flagSet.Output()), + OnCommand: campaignsOnCommand(out, flags.superVerbose), }) if err := svc.DetermineFeatureFlags(ctx); err != nil { diff --git a/internal/campaigns/action.go b/internal/campaigns/action.go index 0891a551b0..0ad65adea5 100644 --- a/internal/campaigns/action.go +++ b/internal/campaigns/action.go @@ -20,7 +20,7 @@ func getDockerImageContentDigest(ctx context.Context, image string) (string, err // digest. but the digest is not calculated for all images (unless they are // pulled/pushed from/to a registry), see // https://github.com/moby/moby/issues/32016. - out, err := exec.CommandContext(ctx, "docker", "image", "inspect", "--format", "{{.Id}}", "--", image).CombinedOutput() + out, err := noticeCommand(ctx, exec.CommandContext(ctx, "docker", "image", "inspect", "--format", "{{.Id}}", "--", image)).CombinedOutput() if err != nil { if !strings.Contains(string(out), "No such image") { return "", fmt.Errorf("error inspecting docker image %q: %s", image, bytes.TrimSpace(out)) diff --git a/internal/campaigns/command.go b/internal/campaigns/command.go new file mode 100644 index 0000000000..276fd18aca --- /dev/null +++ b/internal/campaigns/command.go @@ -0,0 +1,18 @@ +package campaigns + +import ( + "context" + "os/exec" +) + +type OnCommand func(*exec.Cmd) + +const onCommandContextKey = "campaigns.onCommand" + +func noticeCommand(ctx context.Context, cmd *exec.Cmd) *exec.Cmd { + if onCommand, ok := ctx.Value(onCommandContextKey).(OnCommand); onCommand != nil && ok { + onCommand(cmd) + } + + return cmd +} diff --git a/internal/campaigns/run_steps.go b/internal/campaigns/run_steps.go index e53153cf21..45e7413e5e 100644 --- a/internal/campaigns/run_steps.go +++ b/internal/campaigns/run_steps.go @@ -43,7 +43,7 @@ func runSteps(ctx context.Context, wc *WorkspaceCreator, repo *graphql.Repositor "GIT_COMMITTER_EMAIL=campaigns@sourcegraph.com", } cmd.Dir = volumeDir - out, err := cmd.CombinedOutput() + out, err := noticeCommand(ctx, cmd).CombinedOutput() if err != nil { return nil, errors.Wrapf(err, "'git %s' failed: %s", strings.Join(args, " "), out) } @@ -95,7 +95,7 @@ func runSteps(ctx context.Context, wc *WorkspaceCreator, repo *graphql.Repositor if err == nil { ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() - _ = exec.CommandContext(ctx, "docker", "rm", "-f", "--", string(cid)).Run() + _ = noticeCommand(ctx, exec.CommandContext(ctx, "docker", "rm", "-f", "--", string(cid))).Run() } }() @@ -213,7 +213,7 @@ func runSteps(ctx context.Context, wc *WorkspaceCreator, repo *graphql.Repositor logger.Logf("[Step %d] full command: %q", i+1, strings.Join(cmd.Args, " ")) t0 := time.Now() - err = cmd.Run() + err = noticeCommand(ctx, cmd).Run() elapsed := time.Since(t0).Round(time.Millisecond) if err != nil { logger.Logf("[Step %d] took %s; error running Docker container: %+v", i+1, elapsed, err) @@ -293,7 +293,7 @@ func probeImageForShell(ctx context.Context, image string) (shell, tempfile stri cmd.Stdout = stdout cmd.Stderr = stderr - if runErr := cmd.Run(); runErr != nil { + if runErr := noticeCommand(ctx, cmd).Run(); runErr != nil { err = multierror.Append(err, errors.Wrapf(runErr, "probing shell %q:\n%s", shell, stderr.String())) } else { // Even if there were previous errors, we can now ignore them. diff --git a/internal/campaigns/service.go b/internal/campaigns/service.go index 9864cf9b46..553195f425 100644 --- a/internal/campaigns/service.go +++ b/internal/campaigns/service.go @@ -22,11 +22,13 @@ type Service struct { allowUnsupported bool client api.Client features featureFlags + onCommand OnCommand } type ServiceOpts struct { AllowUnsupported bool Client api.Client + OnCommand OnCommand } var ( @@ -37,6 +39,7 @@ func NewService(opts *ServiceOpts) *Service { return &Service{ allowUnsupported: opts.AllowUnsupported, client: opts.Client, + onCommand: opts.OnCommand, } } @@ -68,6 +71,7 @@ func (svc *Service) getSourcegraphVersion(ctx context.Context) (string, error) { // instance and then sets flags on the Service itself to use features available // in that version, e.g. gzip compression. func (svc *Service) DetermineFeatureFlags(ctx context.Context) error { + ctx = svc.decorateContext(ctx) version, err := svc.getSourcegraphVersion(ctx) if err != nil { return errors.Wrap(err, "failed to query Sourcegraph version to check for available features") @@ -95,6 +99,8 @@ mutation ApplyCampaign($campaignSpec: ID!) { ` + graphql.CampaignFieldsFragment func (svc *Service) ApplyCampaign(ctx context.Context, spec CampaignSpecID) (*graphql.Campaign, error) { + ctx = svc.decorateContext(ctx) + var result struct { Campaign *graphql.Campaign `json:"applyCampaign"` } @@ -124,6 +130,8 @@ mutation CreateCampaignSpec( ` func (svc *Service) CreateCampaignSpec(ctx context.Context, namespace, spec string, ids []ChangesetSpecID) (CampaignSpecID, string, error) { + ctx = svc.decorateContext(ctx) + var result struct { CreateCampaignSpec struct { ID string @@ -156,6 +164,8 @@ mutation CreateChangesetSpec($spec: String!) { ` func (svc *Service) CreateChangesetSpec(ctx context.Context, spec *ChangesetSpec) (ChangesetSpecID, error) { + ctx = svc.decorateContext(ctx) + raw, err := json.Marshal(spec) if err != nil { return "", errors.Wrap(err, "marshalling changeset spec JSON") @@ -204,6 +214,8 @@ func (svc *Service) NewWorkspaceCreator(dir string, cleanArchives bool) *Workspa } func (svc *Service) SetDockerImages(ctx context.Context, spec *CampaignSpec, progress func(i int)) error { + ctx = svc.decorateContext(ctx) + for i, step := range spec.Steps { image, err := getDockerImageContentDigest(ctx, step.Container) if err != nil { @@ -217,6 +229,8 @@ func (svc *Service) SetDockerImages(ctx context.Context, spec *CampaignSpec, pro } func (svc *Service) ExecuteCampaignSpec(ctx context.Context, repos []*graphql.Repository, x Executor, spec *CampaignSpec, progress func([]*TaskStatus), skipErrors bool) ([]*ChangesetSpec, error) { + ctx = svc.decorateContext(ctx) + for _, repo := range repos { x.AddTask(repo, spec.Steps, spec.ChangesetTemplate) } @@ -332,6 +346,8 @@ query GetCurrentUserID { ` func (svc *Service) ResolveNamespace(ctx context.Context, namespace string) (string, error) { + ctx = svc.decorateContext(ctx) + if namespace == "" { // if no namespace is provided, default to logged in user as namespace var resp struct { @@ -374,6 +390,8 @@ func (svc *Service) ResolveNamespace(ctx context.Context, namespace string) (str } func (svc *Service) ResolveRepositories(ctx context.Context, spec *CampaignSpec) ([]*graphql.Repository, error) { + ctx = svc.decorateContext(ctx) + seen := map[string]*graphql.Repository{} unsupported := UnsupportedRepoSet{} @@ -421,6 +439,8 @@ func (svc *Service) ResolveRepositories(ctx context.Context, spec *CampaignSpec) } func (svc *Service) ResolveRepositoriesOn(ctx context.Context, on *OnQueryOrRepository) ([]*graphql.Repository, error) { + ctx = svc.decorateContext(ctx) + if on.RepositoriesMatchingQuery != "" { return svc.resolveRepositorySearch(ctx, on.RepositoriesMatchingQuery) } else if on.Repository != "" && on.Branch != "" { @@ -599,3 +619,7 @@ func (sr *searchResult) UnmarshalJSON(data []byte) error { return errors.Errorf("unknown GraphQL type %q", tn.Typename) } } + +func (svc *Service) decorateContext(ctx context.Context) context.Context { + return context.WithValue(ctx, onCommandContextKey, svc.onCommand) +} diff --git a/internal/output/_examples/main.go b/internal/output/_examples/main.go index 640b4bf39b..5e5c2ea012 100644 --- a/internal/output/_examples/main.go +++ b/internal/output/_examples/main.go @@ -11,12 +11,14 @@ import ( var ( duration time.Duration + spinner bool verbose bool withBars bool ) func init() { flag.DurationVar(&duration, "progress", 5*time.Second, "time to take in the progress bar and pending samples") + flag.BoolVar(&spinner, "spinner", true, "enable spinners") flag.BoolVar(&verbose, "verbose", false, "enable verbose mode") flag.BoolVar(&withBars, "with-bars", false, "show status bars on top of progress bar") } @@ -36,52 +38,137 @@ func main() { } func demo(out *output.Output, duration time.Duration) { - var wg sync.WaitGroup - progress := out.Progress([]output.ProgressBar{ - {Label: "A", Max: 1.0}, - {Label: "BB", Max: 1.0, Value: 0.5}, - {Label: strings.Repeat("X", 200), Max: 1.0}, - }, nil) + func() { + var wg sync.WaitGroup + progress := out.Progress([]output.ProgressBar{ + {Label: "A", Max: 1.0}, + {Label: "BB", Max: 1.0, Value: 0.5}, + {Label: strings.Repeat("X", 200), Max: 1.0}, + }, nil) + + wg.Add(1) + go func() { + ticker := time.NewTicker(duration / 20) + defer ticker.Stop() + defer wg.Done() + + i := 0 + for _ = range ticker.C { + i += 1 + if i > 20 { + return + } - wg.Add(1) - go func() { - ticker := time.NewTicker(duration / 20) - defer ticker.Stop() - defer wg.Done() + out.Verbosef("%slog line %d", output.StyleWarning, i) + } + }() + + wg.Add(1) + go func() { + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + defer wg.Done() + + start := time.Now() + until := start.Add(duration) + for _ = range ticker.C { + now := time.Now() + if now.After(until) { + return + } - i := 0 - for _ = range ticker.C { - i += 1 - if i > 20 { - return + progress.SetValue(0, float64(now.Sub(start))/float64(duration)) + progress.SetValue(1, 0.5+float64(now.Sub(start))/float64(duration)/2) + progress.SetValue(2, 2*float64(now.Sub(start))/float64(duration)) } + }() - progress.Verbosef("%slog line %d", output.StyleWarning, i) - } + wg.Wait() + progress.Complete() }() - wg.Add(1) - go func() { - ticker := time.NewTicker(10 * time.Millisecond) - defer ticker.Stop() - defer wg.Done() + func() { + var wg sync.WaitGroup - start := time.Now() - until := start.Add(duration) - for _ = range ticker.C { - now := time.Now() - if now.After(until) { - return - } + progressBars := []output.ProgressBar{ + { + Label: "Executing", + Max: 1.0, + }, + } - progress.SetValue(0, float64(now.Sub(start))/float64(duration)) - progress.SetValue(1, 0.5+float64(now.Sub(start))/float64(duration)/2) - progress.SetValue(2, 2*float64(now.Sub(start))/float64(duration)) + statusBars := []*output.StatusBar{ + output.NewStatusBarWithLabel("AA"), + output.NewStatusBarWithLabel("BB"), + output.NewStatusBarWithLabel(strings.Repeat("X", 200)), } - }() - wg.Wait() - progress.Complete() + progress := out.ProgressWithStatusBars(progressBars, statusBars, output.DefaultProgressTTYOpts.WithNoSpinner(!spinner)) + + wg.Add(1) + go func() { + ticker := time.NewTicker(duration / 20) + defer ticker.Stop() + defer wg.Done() + + i := 0 + for _ = range ticker.C { + i += 1 + if i > 20 { + return + } + + out.Verbosef("%slog line %d", output.StyleWarning, i) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep(duration / 3) + statusBars[0].Completef("done") + }() + + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep(duration / 2) + statusBars[1].Completef("done") + }() + + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep((2 * duration) / 3) + statusBars[2].Completef("done") + }() + + wg.Add(1) + go func() { + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + defer wg.Done() + + start := time.Now() + until := start.Add(duration) + for _ = range ticker.C { + now := time.Now() + if now.After(until) { + return + } + + progress.SetValue(0, float64(now.Sub(start))/float64(duration)) + + for _, bar := range statusBars { + bar.Updatef("%s elapsed", now.Sub(start).String()) + } + } + }() + + wg.Wait() + progress.SetValue(0, float64(duration)) + progress.Complete() + }() func() { ticker := time.NewTicker(10 * time.Millisecond) diff --git a/internal/output/output.go b/internal/output/output.go index f7326a3c25..6ec9898607 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -111,12 +111,18 @@ func (o *Output) VerboseLine(line FancyLine) { func (o *Output) Write(s string) { o.lock.Lock() defer o.lock.Unlock() + if o.caps.Isatty { + o.clearCurrentLine() + } fmt.Fprintln(o.w, s) } func (o *Output) Writef(format string, args ...interface{}) { o.lock.Lock() defer o.lock.Unlock() + if o.caps.Isatty { + o.clearCurrentLine() + } fmt.Fprintf(o.w, format, o.caps.formatArgs(args)...) fmt.Fprint(o.w, "\n") } @@ -124,6 +130,9 @@ func (o *Output) Writef(format string, args ...interface{}) { func (o *Output) WriteLine(line FancyLine) { o.lock.Lock() defer o.lock.Unlock() + if o.caps.Isatty { + o.clearCurrentLine() + } line.write(o.w, o.caps) } diff --git a/internal/output/progress_simple.go b/internal/output/progress_simple.go index cf15aeeab2..806e1f8ccd 100644 --- a/internal/output/progress_simple.go +++ b/internal/output/progress_simple.go @@ -33,16 +33,17 @@ func (p *progressSimple) SetValue(i int, v float64) { } func (p *progressSimple) stop() { - c := make(chan struct{}) - p.done <- c - <-c + if p.done != nil { + c := make(chan struct{}) + p.done <- c + <-c + } } func newProgressSimple(bars []*ProgressBar, o *Output, opts *ProgressOpts) *progressSimple { p := &progressSimple{ Output: o, bars: bars, - done: make(chan chan struct{}), } if opts != nil && opts.NoSpinner { @@ -52,6 +53,7 @@ func newProgressSimple(bars []*ProgressBar, o *Output, opts *ProgressOpts) *prog return p } + p.done = make(chan chan struct{}) go func() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() diff --git a/internal/output/progress_tty.go b/internal/output/progress_tty.go index bdce16a7c9..3449cc61f1 100644 --- a/internal/output/progress_tty.go +++ b/internal/output/progress_tty.go @@ -28,7 +28,9 @@ type progressTTY struct { } func (p *progressTTY) Complete() { - p.spinner.stop() + if p.spinner != nil { + p.spinner.stop() + } p.o.lock.Lock() defer p.o.lock.Unlock() @@ -42,18 +44,17 @@ func (p *progressTTY) Complete() { func (p *progressTTY) Close() { p.Destroy() } func (p *progressTTY) Destroy() { - p.spinner.stop() + if p.spinner != nil { + p.spinner.stop() + } p.o.lock.Lock() defer p.o.lock.Unlock() - p.moveToOrigin() for i := 0; i < len(p.bars); i += 1 { p.o.clearCurrentLine() p.o.moveDown(1) } - - p.moveToOrigin() } func (p *progressTTY) SetLabel(i int, label string) { @@ -62,7 +63,7 @@ func (p *progressTTY) SetLabel(i int, label string) { p.bars[i].Label = label p.bars[i].labelWidth = runewidth.StringWidth(label) - p.drawInSitu() + p.draw() } func (p *progressTTY) SetLabelAndRecalc(i int, label string) { @@ -73,7 +74,7 @@ func (p *progressTTY) SetLabelAndRecalc(i int, label string) { p.bars[i].labelWidth = runewidth.StringWidth(label) p.determineLabelWidth() - p.drawInSitu() + p.draw() } func (p *progressTTY) SetValue(i int, v float64) { @@ -81,7 +82,7 @@ func (p *progressTTY) SetValue(i int, v float64) { defer p.o.lock.Unlock() p.bars[i].Value = v - p.drawInSitu() + p.draw() } func (p *progressTTY) Verbose(s string) { @@ -106,7 +107,6 @@ func (p *progressTTY) Write(s string) { p.o.lock.Lock() defer p.o.lock.Unlock() - p.moveToOrigin() p.o.clearCurrentLine() fmt.Fprintln(p.o.w, s) p.draw() @@ -116,7 +116,6 @@ func (p *progressTTY) Writef(format string, args ...interface{}) { p.o.lock.Lock() defer p.o.lock.Unlock() - p.moveToOrigin() p.o.clearCurrentLine() fmt.Fprintf(p.o.w, format, p.o.caps.formatArgs(args)...) fmt.Fprint(p.o.w, "\n") @@ -127,7 +126,6 @@ func (p *progressTTY) WriteLine(line FancyLine) { p.o.lock.Lock() defer p.o.lock.Unlock() - p.moveToOrigin() p.o.clearCurrentLine() line.write(p.o.w, p.o.caps) p.draw() @@ -139,7 +137,6 @@ func newProgressTTY(bars []*ProgressBar, o *Output, opts *ProgressOpts) *progres o: o, emojiWidth: 3, pendingEmoji: spinnerStrings[0], - spinner: newSpinner(100 * time.Millisecond), } if opts != nil { @@ -160,6 +157,7 @@ func newProgressTTY(bars []*ProgressBar, o *Output, opts *ProgressOpts) *progres return p } + p.spinner = newSpinner(100 * time.Millisecond) go func() { for s := range p.spinner.C { func() { @@ -168,7 +166,6 @@ func newProgressTTY(bars []*ProgressBar, o *Output, opts *ProgressOpts) *progres p.o.lock.Lock() defer p.o.lock.Unlock() - p.moveToOrigin() p.draw() }() } @@ -198,14 +195,14 @@ func (p *progressTTY) determineLabelWidth() { } func (p *progressTTY) draw() { - for _, bar := range p.bars { - p.writeBar(bar) - } + p.drawInSitu() + p.moveToOrigin() } func (p *progressTTY) drawInSitu() { - p.moveToOrigin() - p.draw() + for _, bar := range p.bars { + p.writeBar(bar) + } } func (p *progressTTY) moveToOrigin() { diff --git a/internal/output/progress_with_status_bars_simple.go b/internal/output/progress_with_status_bars_simple.go index 8b41a6b67b..6de4a1ad9c 100644 --- a/internal/output/progress_with_status_bars_simple.go +++ b/internal/output/progress_with_status_bars_simple.go @@ -53,7 +53,6 @@ func newProgressWithStatusBarsSimple(bars []*ProgressBar, statusBars []*StatusBa progressSimple: &progressSimple{ Output: o, bars: bars, - done: make(chan chan struct{}), }, statusBars: statusBars, } @@ -66,6 +65,7 @@ func newProgressWithStatusBarsSimple(bars []*ProgressBar, statusBars []*StatusBa return p } + p.done = make(chan chan struct{}) go func() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() diff --git a/internal/output/progress_with_status_bars_tty.go b/internal/output/progress_with_status_bars_tty.go index c8e57e0a7f..3808507b50 100644 --- a/internal/output/progress_with_status_bars_tty.go +++ b/internal/output/progress_with_status_bars_tty.go @@ -14,7 +14,6 @@ func newProgressWithStatusBarsTTY(bars []*ProgressBar, statusBars []*StatusBar, o: o, emojiWidth: 3, pendingEmoji: spinnerStrings[0], - spinner: newSpinner(100 * time.Millisecond), }, statusBars: statusBars, } @@ -38,6 +37,7 @@ func newProgressWithStatusBarsTTY(bars []*ProgressBar, statusBars []*StatusBar, return p } + p.spinner = newSpinner(100 * time.Millisecond) go func() { for s := range p.spinner.C { func() { @@ -46,7 +46,6 @@ func newProgressWithStatusBarsTTY(bars []*ProgressBar, statusBars []*StatusBar, p.o.lock.Lock() defer p.o.lock.Unlock() - p.moveToOrigin() p.draw() }() } @@ -66,30 +65,31 @@ type progressWithStatusBarsTTY struct { func (p *progressWithStatusBarsTTY) Close() { p.Destroy() } func (p *progressWithStatusBarsTTY) Destroy() { - p.spinner.stop() + if p.spinner != nil { + p.spinner.stop() + } p.o.lock.Lock() defer p.o.lock.Unlock() - p.moveToOrigin() - for i := 0; i < p.lines(); i += 1 { p.o.clearCurrentLine() p.o.moveDown(1) } - p.moveToOrigin() } func (p *progressWithStatusBarsTTY) Complete() { - p.spinner.stop() + if p.spinner != nil { + p.spinner.stop() + } p.o.lock.Lock() defer p.o.lock.Unlock() // +1 because of the line between progress and status bars for i := 0; i < p.numPrintedStatusBars+1; i += 1 { - p.o.moveUp(1) + p.o.moveDown(1) p.o.clearCurrentLine() } p.statusBars = p.statusBars[0:0] @@ -98,8 +98,7 @@ func (p *progressWithStatusBarsTTY) Complete() { bar.Value = bar.Max } - p.o.moveUp(len(p.bars)) - p.draw() + p.o.moveUp(p.numPrintedStatusBars) } func (p *progressWithStatusBarsTTY) lines() int { @@ -112,7 +111,7 @@ func (p *progressWithStatusBarsTTY) SetLabel(i int, label string) { p.bars[i].Label = label p.bars[i].labelWidth = runewidth.StringWidth(label) - p.drawInSitu() + p.draw() } func (p *progressWithStatusBarsTTY) SetValue(i int, v float64) { @@ -120,7 +119,7 @@ func (p *progressWithStatusBarsTTY) SetValue(i int, v float64) { defer p.o.lock.Unlock() p.bars[i].Value = v - p.drawInSitu() + p.draw() } func (p *progressWithStatusBarsTTY) StatusBarResetf(i int, label, format string, args ...interface{}) { @@ -132,7 +131,7 @@ func (p *progressWithStatusBarsTTY) StatusBarResetf(i int, label, format string, } p.determineStatusBarLabelWidth() - p.drawInSitu() + p.draw() } func (p *progressWithStatusBarsTTY) StatusBarUpdatef(i int, format string, args ...interface{}) { @@ -143,7 +142,7 @@ func (p *progressWithStatusBarsTTY) StatusBarUpdatef(i int, format string, args p.statusBars[i].Updatef(format, args...) } - p.drawInSitu() + p.draw() } func (p *progressWithStatusBarsTTY) StatusBarCompletef(i int, format string, args ...interface{}) { @@ -154,7 +153,7 @@ func (p *progressWithStatusBarsTTY) StatusBarCompletef(i int, format string, arg p.statusBars[i].Completef(format, args...) } - p.drawInSitu() + p.draw() } func (p *progressWithStatusBarsTTY) StatusBarFailf(i int, format string, args ...interface{}) { @@ -165,10 +164,19 @@ func (p *progressWithStatusBarsTTY) StatusBarFailf(i int, format string, args .. p.statusBars[i].Failf(format, args...) } - p.drawInSitu() + p.draw() } func (p *progressWithStatusBarsTTY) draw() { + p.drawInSitu() + p.moveToOrigin() +} + +func (p *progressWithStatusBarsTTY) moveToOrigin() { + p.o.moveUp(p.lines()) +} + +func (p *progressWithStatusBarsTTY) drawInSitu() { for _, bar := range p.bars { p.writeBar(bar) } @@ -194,15 +202,6 @@ func (p *progressWithStatusBarsTTY) draw() { } } -func (p *progressWithStatusBarsTTY) moveToOrigin() { - p.o.moveUp(p.lines()) -} - -func (p *progressWithStatusBarsTTY) drawInSitu() { - p.moveToOrigin() - p.draw() -} - func (p *progressWithStatusBarsTTY) determineStatusBarLabelWidth() { p.statusBarLabelWidth = 0 for _, bar := range p.statusBars { @@ -276,7 +275,6 @@ func (p *progressWithStatusBarsTTY) Write(s string) { p.o.lock.Lock() defer p.o.lock.Unlock() - p.moveToOrigin() p.o.clearCurrentLine() fmt.Fprintln(p.o.w, s) p.draw() @@ -286,7 +284,6 @@ func (p *progressWithStatusBarsTTY) Writef(format string, args ...interface{}) { p.o.lock.Lock() defer p.o.lock.Unlock() - p.moveToOrigin() p.o.clearCurrentLine() fmt.Fprintf(p.o.w, format, p.o.caps.formatArgs(args)...) fmt.Fprint(p.o.w, "\n") @@ -297,7 +294,6 @@ func (p *progressWithStatusBarsTTY) WriteLine(line FancyLine) { p.o.lock.Lock() defer p.o.lock.Unlock() - p.moveToOrigin() p.o.clearCurrentLine() line.write(p.o.w, p.o.caps) p.draw()