github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/pkg/terminal/status.go (about) 1 package terminal 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/briandowns/spinner" 12 "github.com/fatih/color" 13 "github.com/morikuni/aec" 14 ) 15 16 const ( 17 StatusOK = "ok" 18 StatusError = "error" 19 StatusWarn = "warn" 20 StatusTimeout = "timeout" 21 StatusAbort = "abort" 22 ) 23 24 var emojiStatus = map[string]string{ 25 StatusOK: "\u2713", 26 StatusError: "❌", 27 StatusWarn: "⚠️", 28 StatusTimeout: "⌛", 29 } 30 31 var textStatus = map[string]string{ 32 StatusOK: " +", 33 StatusError: " !", 34 StatusWarn: " *", 35 StatusTimeout: "<>", 36 } 37 38 var colorStatus = map[string][]aec.ANSI{ 39 StatusOK: {aec.GreenF}, 40 StatusError: {aec.RedF}, 41 StatusWarn: {aec.YellowF}, 42 } 43 44 // Status is used to provide an updating status to the user. The status 45 // usually has some animated element along with it such as a spinner. 46 type Status interface { 47 // Update writes a new status. This should be a single line. 48 Update(msg string) 49 50 // Indicate that a step has finished, confering an ok, error, or warn upon 51 // it's finishing state. If the status is not StatusOK, StatusError, or StatusWarn 52 // then the status text is written directly to the output, allowing for custom 53 // statuses. 54 Step(status, msg string) 55 56 // Close should be called when the live updating is complete. The 57 // status will be cleared from the line. 58 Close() error 59 } 60 61 // spinnerStatus implements Status and uses a spinner to show updates. 62 type spinnerStatus struct { 63 mu sync.Mutex 64 spinner *spinner.Spinner 65 running bool 66 } 67 68 var statusIcons map[string]string 69 70 const envForceEmoji = "WAYPOINT_FORCE_EMOJI" 71 72 func init() { 73 if os.Getenv(envForceEmoji) != "" || strings.Contains(os.Getenv("LANG"), "UTF-8") { 74 statusIcons = emojiStatus 75 } else { 76 statusIcons = textStatus 77 } 78 } 79 80 func newSpinnerStatus(ctx context.Context) *spinnerStatus { 81 return &spinnerStatus{ 82 spinner: spinner.New( 83 spinner.CharSets[11], 84 time.Second/6, 85 spinner.WithColor("bold"), 86 ), 87 } 88 } 89 90 func (s *spinnerStatus) Update(msg string) { 91 s.mu.Lock() 92 defer s.mu.Unlock() 93 94 s.spinner.Suffix = " " + msg 95 96 if !s.running { 97 s.spinner.Start() 98 s.running = true 99 } 100 } 101 102 func (s *spinnerStatus) Step(status, msg string) { 103 s.mu.Lock() 104 defer s.mu.Unlock() 105 106 s.spinner.Stop() 107 s.running = false 108 109 pad := "" 110 111 statusIcon := emojiStatus[status] 112 if statusIcon == "" { 113 statusIcon = status 114 } else if status == StatusWarn { 115 pad = " " 116 } 117 118 fmt.Fprintf(color.Output, "%s%s %s\n", statusIcon, pad, msg) 119 } 120 121 func (s *spinnerStatus) Close() error { 122 s.mu.Lock() 123 defer s.mu.Unlock() 124 125 if s.running { 126 s.running = false 127 s.spinner.Suffix = "" 128 } 129 130 s.spinner.Stop() 131 132 return nil 133 } 134 135 func (s *spinnerStatus) Pause() bool { 136 s.mu.Lock() 137 defer s.mu.Unlock() 138 139 wasRunning := s.running 140 141 if s.running { 142 s.running = false 143 s.spinner.Stop() 144 } 145 146 return wasRunning 147 } 148 149 func (s *spinnerStatus) Start() { 150 s.mu.Lock() 151 defer s.mu.Unlock() 152 153 if !s.running { 154 s.running = true 155 s.spinner.Start() 156 } 157 }