github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/statushooks/spinner.go (about)

     1  package statushooks
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/briandowns/spinner"
    10  	"github.com/fatih/color"
    11  	"github.com/karrick/gows"
    12  	"github.com/turbot/steampipe/pkg/constants"
    13  )
    14  
    15  // spinner format:
    16  // <spinner><space><message><space><dot><dot><dot><cursor>
    17  //
    18  //	1	   1   [.......]   1     1    1    1     1
    19  //
    20  // # We need at least seven characters to show the spinner properly
    21  //
    22  // Not using the (…) character, since it is too small
    23  const minSpinnerWidth = 7
    24  
    25  // StatusSpinner is a struct which implements StatusHooks, and uses a spinner to display status messages
    26  type StatusSpinner struct {
    27  	spinner *spinner.Spinner
    28  	cancel  chan struct{}
    29  	delay   time.Duration
    30  	visible bool
    31  }
    32  
    33  type StatusSpinnerOpt func(*StatusSpinner)
    34  
    35  func WithMessage(msg string) StatusSpinnerOpt {
    36  	return func(s *StatusSpinner) {
    37  		s.UpdateSpinnerMessage(msg)
    38  	}
    39  }
    40  
    41  func WithDelay(delay time.Duration) StatusSpinnerOpt {
    42  	return func(s *StatusSpinner) {
    43  		s.delay = delay
    44  	}
    45  }
    46  
    47  // this is used in the root command to setup a default cmd execution context
    48  // with a status spinner built in
    49  // to update this, use the statushooks.AddStatusHooksToContext
    50  //
    51  // We should never create a StatusSpinner directly. To use a spinner
    52  // DO NOT use a StatusSpinner directly, since using it may have
    53  // unintended side-effect around the spinner lifecycle
    54  func NewStatusSpinnerHook(opts ...StatusSpinnerOpt) *StatusSpinner {
    55  	res := &StatusSpinner{}
    56  
    57  	res.spinner = spinner.New(
    58  		spinner.CharSets[14],
    59  		100*time.Millisecond,
    60  		spinner.WithHiddenCursor(true),
    61  		spinner.WithWriter(os.Stdout),
    62  	)
    63  	for _, opt := range opts {
    64  		opt(res)
    65  	}
    66  
    67  	return res
    68  }
    69  
    70  // SetStatus implements StatusHooks
    71  func (s *StatusSpinner) SetStatus(msg string) {
    72  	s.UpdateSpinnerMessage(msg)
    73  }
    74  
    75  func (s *StatusSpinner) Message(msgs ...string) {
    76  	if s.spinner.Active() {
    77  		s.spinner.Stop()
    78  		defer s.spinner.Start()
    79  	}
    80  	for _, msg := range msgs {
    81  		fmt.Println(msg)
    82  	}
    83  }
    84  
    85  func (s *StatusSpinner) Warn(msg string) {
    86  	if s.spinner.Active() {
    87  		s.spinner.Stop()
    88  		defer s.spinner.Start()
    89  	}
    90  	fmt.Fprintf(color.Output, "%s: %v\n", constants.ColoredWarn, msg)
    91  }
    92  
    93  // Hide implements StatusHooks
    94  func (s *StatusSpinner) Hide() {
    95  	s.visible = false
    96  	if s.cancel != nil {
    97  		close(s.cancel)
    98  	}
    99  	s.closeSpinner()
   100  }
   101  
   102  func (s *StatusSpinner) Show() {
   103  	s.visible = true
   104  	if len(strings.TrimSpace(s.spinner.Suffix)) > 0 {
   105  		// only show the spinner if there's an actual message to show
   106  		s.spinner.Start()
   107  	}
   108  }
   109  
   110  // UpdateSpinnerMessage updates the message of the given spinner
   111  func (s *StatusSpinner) UpdateSpinnerMessage(newMessage string) {
   112  	newMessage = s.truncateSpinnerMessageToScreen(newMessage)
   113  	s.spinner.Suffix = fmt.Sprintf(" %s", newMessage)
   114  	// if the spinner is not active, start it
   115  	if s.visible && !s.spinner.Active() {
   116  		s.spinner.Start()
   117  	}
   118  }
   119  
   120  func (s *StatusSpinner) closeSpinner() {
   121  	if s.spinner != nil {
   122  		s.spinner.Stop()
   123  	}
   124  }
   125  
   126  func (s *StatusSpinner) truncateSpinnerMessageToScreen(msg string) string {
   127  	if len(strings.TrimSpace(msg)) == 0 {
   128  		// if this is a blank message, return it as is
   129  		return msg
   130  	}
   131  
   132  	maxCols, _, _ := gows.GetWinSize()
   133  	// if the screen is smaller than the minimum spinner width, we cannot truncate
   134  	if maxCols < minSpinnerWidth {
   135  		return msg
   136  	}
   137  	availableColumns := maxCols - minSpinnerWidth
   138  	if len(msg) > availableColumns {
   139  		msg = msg[:availableColumns]
   140  		msg = fmt.Sprintf("%s …", msg)
   141  	}
   142  	return msg
   143  }