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 }