Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create a simple spinner for non terminal interactions #4538

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- [#4477](https://github.com/ignite/cli/pull/4477) IBC v10 support
- [#4166](https://github.com/ignite/cli/issues/4166) Migrate buf config files to v2
- [#4494](https://github.com/ignite/cli/pull/4494) Automatic migrate the buf configs to v2
- [#4538](https://github.com/ignite/cli/pull/4538) Create a simple spinner for non-terminal interactions

### Changes

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ require (
github.com/golangci/golangci-lint v1.64.5
github.com/google/go-github/v48 v48.2.0
github.com/google/go-querystring v1.1.0
github.com/gookit/color v1.5.4
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-plugin v1.6.3
github.com/iancoleman/strcase v0.3.0
Expand Down Expand Up @@ -448,6 +449,7 @@ require (
github.com/vbatts/tar-split v0.11.6 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xen0n/gosmopolitan v1.2.2 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.3.0 // indirect
github.com/ykadowak/zerologlint v0.1.5 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,8 @@ github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqE
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
Expand Down Expand Up @@ -1740,6 +1742,8 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI
github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU=
github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=
Expand Down
10 changes: 0 additions & 10 deletions ignite/cmd/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,12 +335,6 @@ func linkPluginCmd(rootCmd *cobra.Command, p *plugin.Plugin, pluginCmd *plugin.C
}
cmd.AddCommand(newCmd)

// NOTE(tb) we could probably simplify by removing this condition and call the
// plugin even if the invoked command isn't runnable. If we do so, the plugin
// will be responsible for outputing the standard cobra output, which implies
// it must use cobra too. This is how cli-plugin-network works, but to make
// it for all, we need to change the `plugin scaffold` output (so it outputs
// something similar than the cli-plugin-network) and update the docs.
if len(pluginCmd.Commands) == 0 {
// pluginCmd has no sub commands, so it's runnable
newCmd.RunE = func(cmd *cobra.Command, args []string) error {
Expand All @@ -362,10 +356,6 @@ func linkPluginCmd(rootCmd *cobra.Command, p *plugin.Plugin, pluginCmd *plugin.C
execCmd.ImportFlags(cmd)
err = p.Interface.Execute(ctx, execCmd, api)

// NOTE(tb): This pause gives enough time for go-plugin to sync the
// output from stdout/stderr of the plugin. Without that pause, this
// output can be discarded and not printed in the user console.
time.Sleep(100 * time.Millisecond)
return err
})
}
Expand Down
113 changes: 37 additions & 76 deletions ignite/pkg/cliui/clispinner/clispinner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,32 @@ package clispinner

import (
"io"
"time"
"os"

"github.com/briandowns/spinner"
"golang.org/x/term"
)

// DefaultText defines the default spinner text.
const DefaultText = "Initializing..."

var (
refreshRate = time.Millisecond * 200
charset = spinner.CharSets[4]
spinnerColor = "blue"
)

type Spinner struct {
sp *spinner.Spinner
}

type (
Spinner interface {
SetText(text string) Spinner
SetPrefix(text string) Spinner
SetCharset(charset []string) Spinner
SetColor(color string) Spinner
Start() Spinner
Stop() Spinner
IsActive() bool
Writer() io.Writer
}

Option func(*Options)

Options struct {
writer io.Writer
text string
writer io.Writer
text string
charset []string
}
)

Expand All @@ -43,76 +45,35 @@ func WithText(text string) Option {
}
}

// WithCharset configures the spinner charset.
func WithCharset(charset []string) Option {
return func(options *Options) {
options.charset = charset
}
}

// New creates a new spinner.
func New(options ...Option) *Spinner {
func New(options ...Option) Spinner {
o := Options{}
for _, apply := range options {
apply(&o)
}

text := o.text
if text == "" {
text = DefaultText
}

spOptions := []spinner.Option{
spinner.WithColor(spinnerColor),
spinner.WithSuffix(" " + text),
if isRunningInTerminal(o.writer) {
return newTermSpinner(o)
}
return newSimpleSpinner(o)
}

if o.writer != nil {
spOptions = append(spOptions, spinner.WithWriter(o.writer))
// isRunningInTerminal check if the writer file descriptor is a terminal.
//
//nolint
func isRunningInTerminal(w io.Writer) bool {
if w == nil {
return term.IsTerminal(int(os.Stdout.Fd()))
}

return &Spinner{
sp: spinner.New(charset, refreshRate, spOptions...),
if f, ok := w.(*os.File); ok {
return term.IsTerminal(int(f.Fd()))
}
}

// SetText sets the text for spinner.
func (s *Spinner) SetText(text string) *Spinner {
s.sp.Lock()
s.sp.Suffix = " " + text
s.sp.Unlock()
return s
}

// SetPrefix sets the prefix for spinner.
func (s *Spinner) SetPrefix(text string) *Spinner {
s.sp.Lock()
s.sp.Prefix = text + " "
s.sp.Unlock()
return s
}

// SetCharset sets the prefix for spinner.
func (s *Spinner) SetCharset(charset []string) *Spinner {
s.sp.UpdateCharSet(charset)
return s
}

// SetColor sets the prefix for spinner.
func (s *Spinner) SetColor(color string) *Spinner {
_ = s.sp.Color(color)
return s
}

// Start starts spinning.
func (s *Spinner) Start() *Spinner {
s.sp.Start()
return s
}

// Stop stops spinning.
func (s *Spinner) Stop() *Spinner {
s.sp.Stop()
s.sp.Prefix = ""
_ = s.sp.Color(spinnerColor)
s.sp.UpdateCharSet(charset)
s.sp.Stop()
return s
}

func (s *Spinner) IsActive() bool {
return s.sp.Active()
return false
}
156 changes: 156 additions & 0 deletions ignite/pkg/cliui/clispinner/simple.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package clispinner

import (
"fmt"
"io"
"os"
"sync"
"time"

"github.com/briandowns/spinner"
"github.com/gookit/color"
)

var (
simpleCharset = spinner.CharSets[4]
simpleRefreshRate = time.Millisecond * 300
simpleColor = color.Blue
)

type SimpleSpinner struct {
mu sync.Mutex
writer io.Writer
charset []string
text string
prefix string
color string
active bool
stopChan chan struct{}
}

// newSimpleSpinner creates a new simple spinner.
func newSimpleSpinner(o Options) *SimpleSpinner {
text := o.text
if text == "" {
text = DefaultText
}

charset := o.charset
if len(charset) == 0 {
charset = simpleCharset
}

writer := o.writer
if writer == nil {
writer = os.Stdout
}

return &SimpleSpinner{
charset: charset,
text: text,
writer: writer,
}
}

// SetText sets the text for the spinner.
func (s *SimpleSpinner) SetText(text string) Spinner {
s.mu.Lock()
s.text = text
s.mu.Unlock()
return s
}

// SetPrefix sets the prefix for the spinner.
func (s *SimpleSpinner) SetPrefix(prefix string) Spinner {
s.mu.Lock()
s.prefix = prefix
s.mu.Unlock()
return s
}

// SetCharset sets the charset for the spinner.
func (s *SimpleSpinner) SetCharset(charset []string) Spinner {
s.mu.Lock()
s.charset = charset
s.mu.Unlock()
return s
}

// SetColor sets the color for the spinner (if color functionality is added).
func (s *SimpleSpinner) SetColor(color string) Spinner {
s.mu.Lock()
s.color = color
s.mu.Unlock()
return s
}

// Start begins the spinner animation.
func (s *SimpleSpinner) Start() Spinner {
s.mu.Lock()
if s.active {
s.mu.Unlock()
return s // Do nothing if already active
}
s.active = true
s.stopChan = make(chan struct{})

// Extract spinner data safely within the mutex
prefix := s.prefix
text := s.text
writer := s.writer
charset := s.charset
s.mu.Unlock()

// Start the animation loop in a separate goroutine
go func() {
ticker := time.NewTicker(simpleRefreshRate)
defer ticker.Stop()

index := 0
for {
select {
case <-s.stopChan: // Stop the spinner
_, _ = fmt.Fprintf(writer, "\r\033[K") // Clear the spinner's line
return
case <-ticker.C: // Update the spinner on each tick
s.mu.Lock()
frame := charset[index]
str := fmt.Sprintf("\r%s%s %s", prefix, simpleColor.Sprint(frame), text)
_, _ = fmt.Fprint(writer, str) // Update the spinner in the same line
index++
if index >= len(charset) {
index = 0
}
s.mu.Unlock()
}
}
}()
return s
}

// Stop ends the spinner animation.
func (s *SimpleSpinner) Stop() Spinner {
s.mu.Lock()
if !s.active {
s.mu.Unlock()
return s // Do nothing if already inactive
}
close(s.stopChan)
s.active = false
s.stopChan = nil
fmt.Print("\r") // Clear spinner line on stop
s.mu.Unlock()
return s
}

// IsActive returns whether the spinner is currently active.
func (s *SimpleSpinner) IsActive() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.active
}

// Writer returns the spinner writer.
func (s *SimpleSpinner) Writer() io.Writer {
return s.writer
}
Loading
Loading