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  }