package statushooks import ( "fmt" "os" "strings" "sync" "time" "github.com/briandowns/spinner" "github.com/fatih/color" "github.com/karrick/gows" "github.com/turbot/pipe-fittings/v2/constants" ) // spinner format: // // // 1 1 [.......] 1 1 1 1 1 // // # We need at least seven characters to show the spinner properly // // Not using the (…) character, since it is too small const minSpinnerWidth = 7 // StatusSpinner is a struct which implements StatusHooks, and uses a spinner to display status messages type StatusSpinner struct { spinner *spinner.Spinner cancel chan struct{} delay time.Duration visible bool mu sync.RWMutex // protects spinner.Suffix and visible fields } type StatusSpinnerOpt func(*StatusSpinner) func WithMessage(msg string) StatusSpinnerOpt { return func(s *StatusSpinner) { s.UpdateSpinnerMessage(msg) } } func WithDelay(delay time.Duration) StatusSpinnerOpt { return func(s *StatusSpinner) { s.delay = delay } } // this is used in the root command to setup a default cmd execution context // with a status spinner built in // to update this, use the statushooks.AddStatusHooksToContext // // We should never create a StatusSpinner directly. To use a spinner // DO NOT use a StatusSpinner directly, since using it may have // unintended side-effect around the spinner lifecycle func NewStatusSpinnerHook(opts ...StatusSpinnerOpt) *StatusSpinner { res := &StatusSpinner{} res.spinner = spinner.New( spinner.CharSets[14], 100*time.Millisecond, spinner.WithHiddenCursor(true), spinner.WithWriter(os.Stdout), ) for _, opt := range opts { opt(res) } return res } // SetStatus implements StatusHooks func (s *StatusSpinner) SetStatus(msg string) { s.UpdateSpinnerMessage(msg) } func (s *StatusSpinner) Message(msgs ...string) { if s.spinner.Active() { s.spinner.Stop() defer s.spinner.Start() } for _, msg := range msgs { fmt.Println(msg) } } func (s *StatusSpinner) Warn(msg string) { if s.spinner.Active() { s.spinner.Stop() defer s.spinner.Start() } fmt.Fprintf(color.Output, "%s: %v\n", constants.ColoredWarn, msg) } // Hide implements StatusHooks func (s *StatusSpinner) Hide() { s.mu.Lock() s.visible = false s.mu.Unlock() if s.cancel != nil { close(s.cancel) } s.closeSpinner() } func (s *StatusSpinner) Show() { s.mu.Lock() defer s.mu.Unlock() s.visible = true if len(strings.TrimSpace(s.spinner.Suffix)) > 0 { // only show the spinner if there's an actual message to show s.spinner.Start() } } // UpdateSpinnerMessage updates the message of the given spinner func (s *StatusSpinner) UpdateSpinnerMessage(newMessage string) { newMessage = s.truncateSpinnerMessageToScreen(newMessage) s.mu.Lock() defer s.mu.Unlock() s.spinner.Suffix = fmt.Sprintf(" %s", newMessage) // if the spinner is not active, start it if s.visible && !s.spinner.Active() { s.spinner.Start() } } func (s *StatusSpinner) closeSpinner() { if s.spinner != nil { s.spinner.Stop() } } func (s *StatusSpinner) truncateSpinnerMessageToScreen(msg string) string { if len(strings.TrimSpace(msg)) == 0 { // if this is a blank message, return it as is return msg } maxCols, _, _ := gows.GetWinSize() // if the screen is smaller than the minimum spinner width, we cannot truncate if maxCols < minSpinnerWidth { return msg } availableColumns := maxCols - minSpinnerWidth if len(msg) > availableColumns { msg = msg[:availableColumns] msg = fmt.Sprintf("%s …", msg) } return msg }