How can I use a spinner in addition to goroutines so that the output is not overriding each other?

Issue

What I am trying to do is use a spinner to indicate work progress in a few goroutines. The issue I am having is that both the spinner message and job completion message is being logged in the same line. What I am asking is how can I have the spinner pinned to the bottom of so that the spinner status is not getting in the way of the logged messages itself?

The output I am getting is

a
⡿ 1/26c
⣽ 2/26b
⣯ 3/26e
⢿ 4/26d
⣾ 5/26g
⣟ 6/26f

What I am trying to get is

a
b
c
d
e
f
⣟ 6/26

Temp working code. (I realize my go routines may not be very elegant, but this is just an example).

package main

import (
    "fmt"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"

    "github.com/theckman/yacspin"
)

func main() {
    // create a spinner
    spinner, err := createSpinner()
    if err != nil {
        fmt.Printf("failed to make spinner from config struct: %v\n", err)
        os.Exit(1)
    }

    // start the spinner
    if err := spinner.Start(); err != nil {
        panic(err)
    }

    wg := &sync.WaitGroup{}
    wg.Add(1)
    var (
        total   int
        current int
    )

    spinnerCh := make(chan int, 0)
    data := make(chan string)

    // only want one go routine at a time but this is not important
    max := make(chan struct{}, 1)

    go func(s *yacspin.Spinner) {
        // tried moving this into the worker goroutine also, but same effect
        for range spinnerCh {
            current += 1
            if err := s.Pause(); err != nil {
                panic(err)
            }
            s.Message(fmt.Sprintf("%d/%d", current, total))
            if err := s.Unpause(); err != nil {
                panic(err)
            }
        }
    }(spinner)

    go func() {
        defer wg.Done()
        for d := range data {
            wg.Add(1)
            go func(wg *sync.WaitGroup, d string) {
                max <- struct{}{}
                defer func() {
                    <-max
                }()

                // function is doing work and printing the result once done.
                fmt.Println(d)

                // sends a value to the spinner go routine so that it can show
                // the updated count
                time.Sleep(500 * time.Millisecond)
                spinnerCh <- 1
                wg.Done()
            }(wg, d)
        }
    }()

    // simulate queing some work
    ss := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
    for _, s := range ss {
        data <- s
    }
    total = len(ss)

    close(data)
    wg.Wait()
    close(spinnerCh)
}

func createSpinner() (*yacspin.Spinner, error) {
    // build the configuration, each field is documented
    cfg := yacspin.Config{
        Frequency: 100 * time.Millisecond,
        CharSet:   yacspin.CharSets[11],
        Suffix:    " ", // puts a least one space between the animating spinner and the Message
        // Message:           "collecting files",
        SuffixAutoColon:   true,
        ColorAll:          true,
        Colors:            []string{"fgYellow"},
        StopCharacter:     "✓",
        StopColors:        []string{"fgGreen"},
        StopMessage:       "done",
        StopFailCharacter: "✗",
        StopFailColors:    []string{"fgRed"},
        StopFailMessage:   "failed",
    }

    s, err := yacspin.New(cfg)
    if err != nil {
        return nil, fmt.Errorf("failed to make spinner from struct: %w", err)
    }

    return s, nil
}

func stopOnSignal(spinner *yacspin.Spinner) {
    // ensure we stop the spinner before exiting, otherwise cursor will remain
    // hidden and terminal will require a `reset`
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
    go func() {
        <-sigCh

        spinner.StopFailMessage("interrupted")

        // ignoring error intentionally
        _ = spinner.StopFail()

        os.Exit(0)
    }()
}

Solution

Based on your initial question, here is the solution for not showing the spinner in the lines:

  1. Introduce a variable

pauseItForAMoment := false which will help to ignore to do Pause or not in one of your goroutine.

func main() {
    pauseItForAMoment := false

And the goroutine looks like this:

        // tried moving this into the worker goroutine also, but same effect
        for range spinnerCh {
            current += 1
            if !pauseItForAMoment {
                if err := s.Pause(); err != nil {
                    panic(err)
                }
                s.Message(fmt.Sprintf("%d/%d", current, total))
                if err := s.Unpause(); err != nil {
                    panic(err)
                }
            }
        }
    }(spinner)
  1. Stop and Start the spinner while printing d
                pauseItForAMoment = true
                spinner.Prefix(d)
                spinner.Stop()
                // fmt.Println(d)
                spinner.Start()
                pauseItForAMoment = false
  1. Change few of the spinner config values to:
        StopCharacter:   " ",
        // StopMessage:       "done",

Note it, your goroutines are not exactly correct to print them in the order, which you know already, hope you will fix that.

Output looks like this:

enter image description here

Here is the code if something is not clear https://go.dev/play/p/0lOza3aapf2

I prefer to show the output like this below & the code for that is https://go.dev/play/p/W8Y1kwNqphl (just my 2cents 😉 ignore it if you don’t need it)

enter image description here

Answered By – Kishore

Answer Checked By – David Marino (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.