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 )