spinner.go 1.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. package progress
  2. import (
  3. "fmt"
  4. "os"
  5. "strings"
  6. "time"
  7. "golang.org/x/term"
  8. )
  9. type Spinner struct {
  10. message string
  11. messageWidth int
  12. parts []string
  13. value int
  14. ticker *time.Ticker
  15. started time.Time
  16. stopped time.Time
  17. }
  18. func NewSpinner(message string) *Spinner {
  19. s := &Spinner{
  20. message: message,
  21. parts: []string{
  22. "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
  23. },
  24. started: time.Now(),
  25. }
  26. go s.start()
  27. return s
  28. }
  29. func (s *Spinner) String() string {
  30. termWidth, _, err := term.GetSize(int(os.Stderr.Fd()))
  31. if err != nil {
  32. panic(err)
  33. }
  34. var pre strings.Builder
  35. if len(s.message) > 0 {
  36. message := strings.TrimSpace(s.message)
  37. if s.messageWidth > 0 && len(message) > s.messageWidth {
  38. message = message[:s.messageWidth]
  39. }
  40. fmt.Fprintf(&pre, "%s", message)
  41. if s.messageWidth-pre.Len() >= 0 {
  42. pre.WriteString(strings.Repeat(" ", s.messageWidth-pre.Len()))
  43. }
  44. pre.WriteString(" ")
  45. }
  46. var pad int
  47. if s.stopped.IsZero() {
  48. // spinner has a string length of 3 but a rune length of 1
  49. // in order to align correctly, we need to pad with (3 - 1) = 2 spaces
  50. spinner := s.parts[s.value]
  51. pre.WriteString(spinner)
  52. pad = len(spinner) - len([]rune(spinner))
  53. }
  54. var suf strings.Builder
  55. fmt.Fprintf(&suf, "(%s)", s.elapsed())
  56. var mid strings.Builder
  57. f := termWidth - pre.Len() - mid.Len() - suf.Len() + pad
  58. if f > 0 {
  59. mid.WriteString(strings.Repeat(" ", f))
  60. }
  61. return pre.String() + mid.String() + suf.String()
  62. }
  63. func (s *Spinner) start() {
  64. s.ticker = time.NewTicker(100 * time.Millisecond)
  65. for range s.ticker.C {
  66. s.value = (s.value + 1) % len(s.parts)
  67. if !s.stopped.IsZero() {
  68. return
  69. }
  70. }
  71. }
  72. func (s *Spinner) Stop() {
  73. if s.stopped.IsZero() {
  74. s.stopped = time.Now()
  75. }
  76. }
  77. func (s *Spinner) elapsed() time.Duration {
  78. stopped := s.stopped
  79. if stopped.IsZero() {
  80. stopped = time.Now()
  81. }
  82. return stopped.Sub(s.started).Round(time.Second)
  83. }