github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/pkg/terminal/ui.go (about)

     1  package terminal
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  
     8  	"github.com/fatih/color"
     9  )
    10  
    11  // ErrNonInteractive is returned when Input is called on a non-Interactive UI.
    12  var ErrNonInteractive = errors.New("noninteractive UI doesn't support this operation")
    13  
    14  // Passed to UI.NamedValues to provide a nicely formatted key: value output
    15  type NamedValue struct {
    16  	Name  string
    17  	Value interface{}
    18  }
    19  
    20  // UI is the primary interface for interacting with a user via the CLI.
    21  //
    22  // Some of the methods on this interface return values that have a lifetime
    23  // such as Status and StepGroup. While these are still active (haven't called
    24  // the close or equivalent method on these values), no other method on the
    25  // UI should be called.
    26  type UI interface {
    27  	// Input asks the user for input. This will immediately return an error
    28  	// if the UI doesn't support interaction. You can test for interaction
    29  	// ahead of time with Interactive().
    30  	Input(*Input) (string, error)
    31  
    32  	// Interactive returns true if this prompt supports user interaction.
    33  	// If this is false, Input will always error.
    34  	Interactive() bool
    35  
    36  	// Output outputs a message directly to the terminal. The remaining
    37  	// arguments should be interpolations for the format string. After the
    38  	// interpolations you may add Options.
    39  	Output(string, ...interface{})
    40  
    41  	// Output data as a table of data. Each entry is a row which will be output
    42  	// with the columns lined up nicely.
    43  	NamedValues([]NamedValue, ...Option)
    44  
    45  	// OutputWriters returns stdout and stderr writers. These are usually
    46  	// but not always TTYs. This is useful for subprocesses, network requests,
    47  	// etc. Note that writing to these is not thread-safe by default so
    48  	// you must take care that there is only ever one writer.
    49  	OutputWriters() (stdout, stderr io.Writer, err error)
    50  
    51  	// Status returns a live-updating status that can be used for single-line
    52  	// status updates that typically have a spinner or some similar style.
    53  	// While a Status is live (Close isn't called), other methods on UI should
    54  	// NOT be called.
    55  	Status() Status
    56  
    57  	// Table outputs the information formatted into a Table structure.
    58  	Table(*Table, ...Option)
    59  
    60  	// StepGroup returns a value that can be used to output individual (possibly
    61  	// parallel) steps that have their own message, status indicator, spinner, and
    62  	// body. No other output mechanism (Output, Input, Status, etc.) may be
    63  	// called until the StepGroup is complete.
    64  	StepGroup() StepGroup
    65  }
    66  
    67  // StepGroup is a group of steps (that may be concurrent).
    68  type StepGroup interface {
    69  	// Start a step in the output with the arguments making up the initial message
    70  	Add(string, ...interface{}) Step
    71  
    72  	// Wait for all steps to finish. This allows a StepGroup to be used like
    73  	// a sync.WaitGroup with each step being run in a separate goroutine.
    74  	// This must be called to properly clean up the step group.
    75  	Wait()
    76  }
    77  
    78  // A Step is the unit of work within a StepGroup. This can be driven by concurrent
    79  // goroutines safely.
    80  type Step interface {
    81  	// The Writer has data written to it as though it was a terminal. This will appear
    82  	// as body text under the Step's message and status.
    83  	TermOutput() io.Writer
    84  
    85  	// Change the Steps displayed message
    86  	Update(string, ...interface{})
    87  
    88  	// Update the status of the message. Supported values are in status.go.
    89  	Status(status string)
    90  
    91  	// Called when the step has finished. This must be done otherwise the StepGroup
    92  	// will wait forever for it's Steps to finish.
    93  	Done()
    94  
    95  	// Sets the status to Error and finishes the Step if it's not already done.
    96  	// This is usually done in a defer so that any return before the Done() shows
    97  	// the Step didn't completely properly.
    98  	Abort()
    99  }
   100  
   101  // Interpret decomposes the msg and arguments into the message, style, and writer
   102  func Interpret(msg string, raw ...interface{}) (string, string, io.Writer) {
   103  	// Build our args and options
   104  	var args []interface{}
   105  	var opts []Option
   106  	for _, r := range raw {
   107  		if opt, ok := r.(Option); ok {
   108  			opts = append(opts, opt)
   109  		} else {
   110  			args = append(args, r)
   111  		}
   112  	}
   113  
   114  	// Build our message
   115  	msg = fmt.Sprintf(msg, args...)
   116  
   117  	// Build our config and set our options
   118  	cfg := &config{Writer: color.Output}
   119  	for _, opt := range opts {
   120  		opt(cfg)
   121  	}
   122  
   123  	return msg, cfg.Style, cfg.Writer
   124  }
   125  
   126  const (
   127  	HeaderStyle      = "header"
   128  	ErrorStyle       = "error"
   129  	ErrorBoldStyle   = "error-bold"
   130  	WarningStyle     = "warning"
   131  	WarningBoldStyle = "warning-bold"
   132  	InfoStyle        = "info"
   133  	SuccessStyle     = "success"
   134  	SuccessBoldStyle = "success-bold"
   135  )
   136  
   137  type config struct {
   138  	// Writer is where the message will be written to.
   139  	Writer io.Writer
   140  
   141  	// The style the output should take on
   142  	Style string
   143  }
   144  
   145  // Option controls output styling.
   146  type Option func(*config)
   147  
   148  // WithHeaderStyle styles the output like a header denoting a new section
   149  // of execution. This should only be used with single-line output. Multi-line
   150  // output will not look correct.
   151  func WithHeaderStyle() Option {
   152  	return func(c *config) {
   153  		c.Style = HeaderStyle
   154  	}
   155  }
   156  
   157  // WithInfoStyle styles the output like it's formatted information.
   158  func WithInfoStyle() Option {
   159  	return func(c *config) {
   160  		c.Style = InfoStyle
   161  	}
   162  }
   163  
   164  // WithErrorStyle styles the output as an error message.
   165  func WithErrorStyle() Option {
   166  	return func(c *config) {
   167  		c.Style = ErrorStyle
   168  	}
   169  }
   170  
   171  // WithWarningStyle styles the output as an error message.
   172  func WithWarningStyle() Option {
   173  	return func(c *config) {
   174  		c.Style = WarningStyle
   175  	}
   176  }
   177  
   178  // WithSuccessStyle styles the output as a success message.
   179  func WithSuccessStyle() Option {
   180  	return func(c *config) {
   181  		c.Style = SuccessStyle
   182  	}
   183  }
   184  
   185  func WithStyle(style string) Option {
   186  	return func(c *config) {
   187  		c.Style = style
   188  	}
   189  }
   190  
   191  // WithWriter specifies the writer for the output.
   192  func WithWriter(w io.Writer) Option {
   193  	return func(c *config) { c.Writer = w }
   194  }
   195  
   196  var (
   197  	colorHeader      = color.New(color.Bold)
   198  	colorInfo        = color.New()
   199  	colorError       = color.New(color.FgRed)
   200  	colorErrorBold   = color.New(color.FgRed, color.Bold)
   201  	colorSuccess     = color.New(color.FgGreen)
   202  	colorSuccessBold = color.New(color.FgGreen, color.Bold)
   203  	colorWarning     = color.New(color.FgYellow)
   204  	colorWarningBold = color.New(color.FgYellow, color.Bold)
   205  )