github.com/aacfactory/fns@v1.2.86-0.20240310083819-80d667fc0a17/cmd/generates/spinner/spin.go (about) 1 /* 2 * Copyright 2023 Wang Min Xiang 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package spinner 19 20 import ( 21 "errors" 22 "fmt" 23 "io" 24 "math" 25 "os" 26 "runtime" 27 "strconv" 28 "strings" 29 "sync" 30 "time" 31 "unicode/utf8" 32 33 "github.com/fatih/color" 34 "github.com/mattn/go-isatty" 35 "golang.org/x/term" 36 ) 37 38 // errInvalidColor is returned when attempting to set an invalid color 39 var errInvalidColor = errors.New("invalid color") 40 41 // validColors holds an array of the only colors allowed 42 var validColors = map[string]bool{ 43 // default colors for backwards compatibility 44 "black": true, 45 "red": true, 46 "green": true, 47 "yellow": true, 48 "blue": true, 49 "magenta": true, 50 "cyan": true, 51 "white": true, 52 53 // attributes 54 "reset": true, 55 "bold": true, 56 "faint": true, 57 "italic": true, 58 "underline": true, 59 "blinkslow": true, 60 "blinkrapid": true, 61 "reversevideo": true, 62 "concealed": true, 63 "crossedout": true, 64 65 // foreground text 66 "fgBlack": true, 67 "fgRed": true, 68 "fgGreen": true, 69 "fgYellow": true, 70 "fgBlue": true, 71 "fgMagenta": true, 72 "fgCyan": true, 73 "fgWhite": true, 74 75 // foreground Hi-Intensity text 76 "fgHiBlack": true, 77 "fgHiRed": true, 78 "fgHiGreen": true, 79 "fgHiYellow": true, 80 "fgHiBlue": true, 81 "fgHiMagenta": true, 82 "fgHiCyan": true, 83 "fgHiWhite": true, 84 85 // background text 86 "bgBlack": true, 87 "bgRed": true, 88 "bgGreen": true, 89 "bgYellow": true, 90 "bgBlue": true, 91 "bgMagenta": true, 92 "bgCyan": true, 93 "bgWhite": true, 94 95 // background Hi-Intensity text 96 "bgHiBlack": true, 97 "bgHiRed": true, 98 "bgHiGreen": true, 99 "bgHiYellow": true, 100 "bgHiBlue": true, 101 "bgHiMagenta": true, 102 "bgHiCyan": true, 103 "bgHiWhite": true, 104 } 105 106 var isWindows = runtime.GOOS == "windows" 107 var isWindowsTerminalOnWindows = len(os.Getenv("WT_SESSION")) > 0 && isWindows 108 109 var colorAttributeMap = map[string]color.Attribute{ 110 "black": color.FgBlack, 111 "red": color.FgRed, 112 "green": color.FgGreen, 113 "yellow": color.FgYellow, 114 "blue": color.FgBlue, 115 "magenta": color.FgMagenta, 116 "cyan": color.FgCyan, 117 "white": color.FgWhite, 118 "reset": color.Reset, 119 "bold": color.Bold, 120 "faint": color.Faint, 121 "italic": color.Italic, 122 "underline": color.Underline, 123 "blinkslow": color.BlinkSlow, 124 "blinkrapid": color.BlinkRapid, 125 "reversevideo": color.ReverseVideo, 126 "concealed": color.Concealed, 127 "crossedout": color.CrossedOut, 128 "fgBlack": color.FgBlack, 129 "fgRed": color.FgRed, 130 "fgGreen": color.FgGreen, 131 "fgYellow": color.FgYellow, 132 "fgBlue": color.FgBlue, 133 "fgMagenta": color.FgMagenta, 134 "fgCyan": color.FgCyan, 135 "fgWhite": color.FgWhite, 136 "fgHiBlack": color.FgHiBlack, 137 "fgHiRed": color.FgHiRed, 138 "fgHiGreen": color.FgHiGreen, 139 "fgHiYellow": color.FgHiYellow, 140 "fgHiBlue": color.FgHiBlue, 141 "fgHiMagenta": color.FgHiMagenta, 142 "fgHiCyan": color.FgHiCyan, 143 "fgHiWhite": color.FgHiWhite, 144 "bgBlack": color.BgBlack, 145 "bgRed": color.BgRed, 146 "bgGreen": color.BgGreen, 147 "bgYellow": color.BgYellow, 148 "bgBlue": color.BgBlue, 149 "bgMagenta": color.BgMagenta, 150 "bgCyan": color.BgCyan, 151 "bgWhite": color.BgWhite, 152 "bgHiBlack": color.BgHiBlack, 153 "bgHiRed": color.BgHiRed, 154 "bgHiGreen": color.BgHiGreen, 155 "bgHiYellow": color.BgHiYellow, 156 "bgHiBlue": color.BgHiBlue, 157 "bgHiMagenta": color.BgHiMagenta, 158 "bgHiCyan": color.BgHiCyan, 159 "bgHiWhite": color.BgHiWhite, 160 } 161 162 func validColor(c string) bool { 163 return validColors[c] 164 } 165 166 type Spinner struct { 167 mu *sync.RWMutex 168 Delay time.Duration 169 chars []string 170 Prefix string 171 Suffix string 172 FinalMSG string 173 lastOutputPlain string 174 LastOutput string 175 color func(a ...interface{}) string 176 Writer io.Writer 177 WriterFile *os.File 178 active bool 179 enabled bool 180 stopChan chan struct{} 181 HideCursor bool 182 PreUpdate func(s *Spinner) 183 PostUpdate func(s *Spinner) 184 } 185 186 func New(cs []string, d time.Duration, options ...Option) *Spinner { 187 s := &Spinner{ 188 Delay: d, 189 chars: cs, 190 color: color.New(color.FgWhite).SprintFunc(), 191 mu: &sync.RWMutex{}, 192 Writer: color.Output, 193 WriterFile: os.Stdout, 194 stopChan: make(chan struct{}, 1), 195 active: false, 196 enabled: true, 197 HideCursor: true, 198 } 199 200 for _, option := range options { 201 option(s) 202 } 203 204 return s 205 } 206 207 type Option func(*Spinner) 208 209 type Options struct { 210 Color string 211 Suffix string 212 FinalMSG string 213 HideCursor bool 214 } 215 216 func WithColor(color string) Option { 217 return func(s *Spinner) { 218 s.Color(color) 219 } 220 } 221 222 func WithSuffix(suffix string) Option { 223 return func(s *Spinner) { 224 s.Suffix = suffix 225 } 226 } 227 228 func WithFinalMSG(finalMsg string) Option { 229 return func(s *Spinner) { 230 s.FinalMSG = finalMsg 231 } 232 } 233 234 func WithHiddenCursor(hideCursor bool) Option { 235 return func(s *Spinner) { 236 s.HideCursor = hideCursor 237 } 238 } 239 240 func WithWriter(w io.Writer) Option { 241 return func(s *Spinner) { 242 s.mu.Lock() 243 s.Writer = w 244 s.WriterFile = os.Stdout 245 s.mu.Unlock() 246 } 247 } 248 249 func (s *Spinner) Active() bool { 250 return s.active 251 } 252 253 func (s *Spinner) Enabled() bool { 254 return s.enabled 255 } 256 257 func (s *Spinner) Enable() { 258 s.enabled = true 259 s.Restart() 260 } 261 262 func (s *Spinner) Disable() { 263 s.enabled = false 264 s.Stop() 265 } 266 267 func (s *Spinner) Start() { 268 s.mu.Lock() 269 if s.active || !s.enabled || !isRunningInTerminal(s) { 270 s.mu.Unlock() 271 return 272 } 273 if s.HideCursor && !isWindowsTerminalOnWindows { 274 fmt.Fprint(s.Writer, "\033[?25l") 275 } 276 if isWindows && !isWindowsTerminalOnWindows { 277 color.NoColor = true 278 } 279 280 s.active = true 281 s.mu.Unlock() 282 283 go func() { 284 for { 285 for i := 0; i < len(s.chars); i++ { 286 select { 287 case <-s.stopChan: 288 return 289 default: 290 s.mu.Lock() 291 if !s.active { 292 s.mu.Unlock() 293 return 294 } 295 if !isWindowsTerminalOnWindows { 296 s.erase() 297 } 298 299 if s.PreUpdate != nil { 300 s.PreUpdate(s) 301 } 302 303 var outColor string 304 if isWindows { 305 if s.Writer == os.Stderr { 306 outColor = fmt.Sprintf("\r%s%s%s", s.Prefix, s.chars[i], s.Suffix) 307 } else { 308 outColor = fmt.Sprintf("\r%s%s%s", s.Prefix, s.color(s.chars[i]), s.Suffix) 309 } 310 } else { 311 outColor = fmt.Sprintf("\r%s%s%s", s.Prefix, s.color(s.chars[i]), s.Suffix) 312 } 313 outPlain := fmt.Sprintf("\r%s%s%s", s.Prefix, s.chars[i], s.Suffix) 314 fmt.Fprint(s.Writer, outColor) 315 s.lastOutputPlain = outPlain 316 s.LastOutput = outColor 317 delay := s.Delay 318 319 if s.PostUpdate != nil { 320 s.PostUpdate(s) 321 } 322 323 s.mu.Unlock() 324 time.Sleep(delay) 325 } 326 } 327 } 328 }() 329 } 330 331 func (s *Spinner) Stop() { 332 s.mu.Lock() 333 defer s.mu.Unlock() 334 if s.active { 335 s.active = false 336 if s.HideCursor && !isWindowsTerminalOnWindows { 337 // makes the cursor visible 338 fmt.Fprint(s.Writer, "\033[?25h") 339 } 340 s.erase() 341 if s.FinalMSG != "" { 342 if isWindowsTerminalOnWindows { 343 fmt.Fprint(s.Writer, "\r", s.FinalMSG) 344 } else { 345 fmt.Fprint(s.Writer, s.FinalMSG) 346 } 347 } 348 s.stopChan <- struct{}{} 349 } 350 } 351 352 func (s *Spinner) Restart() { 353 s.Stop() 354 s.Start() 355 } 356 357 func (s *Spinner) Reverse() { 358 s.mu.Lock() 359 for i, j := 0, len(s.chars)-1; i < j; i, j = i+1, j-1 { 360 s.chars[i], s.chars[j] = s.chars[j], s.chars[i] 361 } 362 s.mu.Unlock() 363 } 364 365 func (s *Spinner) Color(colors ...string) error { 366 colorAttributes := make([]color.Attribute, len(colors)) 367 368 for index, c := range colors { 369 if !validColor(c) { 370 return errInvalidColor 371 } 372 colorAttributes[index] = colorAttributeMap[c] 373 } 374 375 s.mu.Lock() 376 s.color = color.New(colorAttributes...).SprintFunc() 377 s.mu.Unlock() 378 return nil 379 } 380 381 func (s *Spinner) UpdateSpeed(d time.Duration) { 382 s.mu.Lock() 383 s.Delay = d 384 s.mu.Unlock() 385 } 386 387 func (s *Spinner) UpdateCharSet(cs []string) { 388 s.mu.Lock() 389 s.chars = cs 390 s.mu.Unlock() 391 } 392 393 func (s *Spinner) erase() { 394 n := utf8.RuneCountInString(s.lastOutputPlain) 395 if runtime.GOOS == "windows" && !isWindowsTerminalOnWindows { 396 clearString := "\r" + strings.Repeat(" ", n) + "\r" 397 fmt.Fprint(s.Writer, clearString) 398 s.lastOutputPlain = "" 399 return 400 } 401 402 numberOfLinesToErase := computeNumberOfLinesNeededToPrintString(s.lastOutputPlain) 403 404 eraseCodeString := strings.Builder{} 405 eraseCodeString.WriteString("\r\033[K") // start by erasing current line 406 for i := 1; i < numberOfLinesToErase; i++ { 407 eraseCodeString.WriteString("\033[F\033[K") 408 } 409 fmt.Fprintf(s.Writer, eraseCodeString.String()) 410 s.lastOutputPlain = "" 411 } 412 413 func (s *Spinner) Lock() { 414 s.mu.Lock() 415 } 416 417 func (s *Spinner) Unlock() { 418 s.mu.Unlock() 419 } 420 421 func GenerateNumberSequence(length int) []string { 422 numSeq := make([]string, length) 423 for i := 0; i < length; i++ { 424 numSeq[i] = strconv.Itoa(i) 425 } 426 return numSeq 427 } 428 429 func isRunningInTerminal(s *Spinner) bool { 430 return isatty.IsTerminal(s.WriterFile.Fd()) 431 } 432 433 func computeNumberOfLinesNeededToPrintString(linePrinted string) int { 434 terminalWidth := math.MaxInt 435 if term.IsTerminal(0) { 436 if width, _, err := term.GetSize(0); err == nil { 437 terminalWidth = width 438 } 439 } 440 return computeNumberOfLinesNeededToPrintStringInternal(linePrinted, terminalWidth) 441 } 442 443 func isAnsiMarker(r rune) bool { 444 return r == '\x1b' 445 } 446 447 func isAnsiTerminator(r rune) bool { 448 return (r >= 0x40 && r <= 0x5a) || (r == 0x5e) || (r >= 0x60 && r <= 0x7e) 449 } 450 451 func computeLineWidth(line string) int { 452 width := 0 453 ansi := false 454 455 for _, r := range []rune(line) { 456 if ansi || isAnsiMarker(r) { 457 ansi = !isAnsiTerminator(r) 458 } else { 459 width += utf8.RuneLen(r) 460 } 461 } 462 463 return width 464 } 465 466 func computeNumberOfLinesNeededToPrintStringInternal(linePrinted string, maxLineWidth int) int { 467 lineCount := 0 468 for _, line := range strings.Split(linePrinted, "\n") { 469 lineCount += 1 470 471 lineWidth := computeLineWidth(line) 472 if lineWidth > maxLineWidth { 473 lineCount += int(float64(lineWidth) / float64(maxLineWidth)) 474 } 475 } 476 477 return lineCount 478 }