github.com/dcarley/cf-cli@v6.24.1-0.20170220111324-4225ff346898+incompatible/util/ui/ui.go (about) 1 // Package ui will provide hooks into STDOUT, STDERR and STDIN. It will also 2 // handle translation as necessary. 3 // 4 // This package is explicitly designed for the CF CLI and is *not* to be used 5 // by any package outside of the commands package. 6 package ui 7 8 import ( 9 "fmt" 10 "io" 11 "os" 12 "strings" 13 "time" 14 15 "code.cloudfoundry.org/cli/util/configv3" 16 "github.com/fatih/color" 17 runewidth "github.com/mattn/go-runewidth" 18 "github.com/nicksnyder/go-i18n/i18n" 19 "github.com/vito/go-interact/interact" 20 ) 21 22 const ( 23 red color.Attribute = color.FgRed 24 green = color.FgGreen 25 // yellow = color.FgYellow 26 // magenta = color.FgMagenta 27 cyan = color.FgCyan 28 white = color.FgWhite 29 defaultFgColor = 38 30 ) 31 32 //go:generate counterfeiter . Config 33 34 // Config is the UI configuration 35 type Config interface { 36 // ColorEnabled enables or disabled color 37 ColorEnabled() configv3.ColorSetting 38 // Locale is the language to translate the output to 39 Locale() string 40 } 41 42 //go:generate counterfeiter . TranslatableError 43 44 // TranslatableError it wraps the error interface adding a way to set the 45 // translation function on the error 46 type TranslatableError interface { 47 // Returns back the untranslated error string 48 Error() string 49 Translate(func(string, ...interface{}) string) string 50 } 51 52 //go:generate counterfeiter . LogMessage 53 54 // LogMessage is a log response representing one to many joined lines of a log 55 // message. 56 type LogMessage interface { 57 Message() string 58 Type() string 59 Timestamp() time.Time 60 SourceType() string 61 SourceInstance() string 62 } 63 64 // UI is interface to interact with the user 65 type UI struct { 66 // In is the input buffer 67 In io.Reader 68 // Out is the output buffer 69 Out io.Writer 70 // Err is the error buffer 71 Err io.Writer 72 73 colorEnabled configv3.ColorSetting 74 translate i18n.TranslateFunc 75 76 TimezoneLocation *time.Location 77 } 78 79 // NewUI will return a UI object where Out is set to STDOUT, In is set to 80 // STDIN, and Err is set to STDERR 81 func NewUI(c Config) (*UI, error) { 82 translateFunc, err := GetTranslationFunc(c) 83 if err != nil { 84 return nil, err 85 } 86 87 location := time.Now().Location() 88 89 return &UI{ 90 In: os.Stdin, 91 Out: color.Output, 92 Err: os.Stderr, 93 colorEnabled: c.ColorEnabled(), 94 translate: translateFunc, 95 TimezoneLocation: location, 96 }, nil 97 } 98 99 // NewTestUI will return a UI object where Out, In, and Err are customizable, 100 // and colors are disabled 101 func NewTestUI(in io.Reader, out io.Writer, err io.Writer) *UI { 102 return &UI{ 103 In: in, 104 Out: out, 105 Err: err, 106 colorEnabled: configv3.ColorDisabled, 107 translate: translationWrapper(i18n.IdentityTfunc()), 108 } 109 } 110 111 // TranslateText passes the template through an internationalization function 112 // to translate it to a pre-configured language, and returns the template with 113 // templateValues substituted in. Only the first map in templateValues is used. 114 func (ui *UI) TranslateText(template string, templateValues ...map[string]interface{}) string { 115 return ui.translate(template, getFirstSet(templateValues)) 116 } 117 118 // UserFriendlyDate converts the time to UTC and then formats it to ISO8601. 119 func (ui *UI) UserFriendlyDate(input time.Time) string { 120 return input.UTC().Format(time.RFC3339) 121 } 122 123 // DisplayOK outputs a bold green translated "OK" to UI.Out. 124 func (ui *UI) DisplayOK() { 125 fmt.Fprintf(ui.Out, "%s\n", ui.addFlavor(ui.TranslateText("OK"), green, true)) 126 } 127 128 // DisplayNewline outputs a newline to UI.Out. 129 func (ui *UI) DisplayNewline() { 130 fmt.Fprintf(ui.Out, "\n") 131 } 132 133 // DisplayBoolPrompt outputs the prompt and waits for user input. It only 134 // allows for a boolean response. A default boolean response can be set with 135 // defaultResponse. 136 func (ui *UI) DisplayBoolPrompt(prompt string, defaultResponse bool) (bool, error) { 137 response := defaultResponse 138 interactivePrompt := interact.NewInteraction(fmt.Sprintf("%s%s", prompt, ui.addFlavor(">>", cyan, true))) 139 interactivePrompt.Input = ui.In 140 interactivePrompt.Output = ui.Out 141 err := interactivePrompt.Resolve(&response) 142 return response, err 143 } 144 145 // DisplayTable outputs a matrix of strings as a table to UI.Out. Prefix will 146 // be prepended to each row and padding adds the specified number of spaces 147 // between columns. 148 func (ui *UI) DisplayTable(prefix string, table [][]string, padding int) { 149 if len(table) == 0 { 150 return 151 } 152 153 var columnPadding []int 154 155 rows := len(table) 156 columns := len(table[0]) 157 for col := 0; col < columns; col++ { 158 var max int 159 for row := 0; row < rows; row++ { 160 if strLen := runewidth.StringWidth(table[row][col]); max < strLen { 161 max = strLen 162 } 163 } 164 columnPadding = append(columnPadding, max+padding) 165 } 166 167 for row := 0; row < rows; row++ { 168 fmt.Fprintf(ui.Out, prefix) 169 for col := 0; col < columns; col++ { 170 var addedPadding int 171 if col+1 != columns { 172 addedPadding = columnPadding[col] - runewidth.StringWidth(table[row][col]) 173 } 174 fmt.Fprintf(ui.Out, "%s%s", table[row][col], strings.Repeat(" ", addedPadding)) 175 } 176 fmt.Fprintf(ui.Out, "\n") 177 } 178 } 179 180 // DisplayText translates the template, substitutes in templateValues, and 181 // outputs the result to ui.Out. Only the first map in templateValues is used. 182 func (ui *UI) DisplayText(template string, templateValues ...map[string]interface{}) { 183 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, templateValues...)) 184 } 185 186 // DisplayPair translates the attribute, translates the template, substitutes 187 // templateValues into the template, and outputs the pair to ui.Out. Only the 188 // first map in templateValues is used. 189 func (ui *UI) DisplayPair(attribute string, template string, templateValues ...map[string]interface{}) { 190 fmt.Fprintf(ui.Out, "%s: %s\n", ui.TranslateText(attribute), ui.TranslateText(template, templateValues...)) 191 } 192 193 // DisplayHeader translates the header, bolds and adds the default color to the 194 // header, and outputs the result to ui.Out. 195 func (ui *UI) DisplayHeader(text string) { 196 fmt.Fprintf(ui.Out, "%s\n", ui.addFlavor(ui.TranslateText(text), defaultFgColor, true)) 197 } 198 199 // DisplayTextWithFlavor translates the template, bolds and adds cyan color to 200 // templateValues, substitutes templateValues into the template, and outputs 201 // the result to ui.Out. Only the first map in templateValues is used. 202 func (ui *UI) DisplayTextWithFlavor(template string, templateValues ...map[string]interface{}) { 203 firstTemplateValues := getFirstSet(templateValues) 204 for key, value := range firstTemplateValues { 205 firstTemplateValues[key] = ui.addFlavor(fmt.Sprint(value), cyan, true) 206 } 207 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues)) 208 } 209 210 // DisplayWarning translates the warning, substitutes in templateValues, and 211 // outputs to ui.Err. Only the first map in templateValues is used. 212 func (ui *UI) DisplayWarning(template string, templateValues ...map[string]interface{}) { 213 fmt.Fprintf(ui.Err, "%s\n", ui.TranslateText(template, templateValues...)) 214 } 215 216 // DisplayWarnings translates the warnings and outputs to ui.Err. 217 func (ui *UI) DisplayWarnings(warnings []string) { 218 for _, warning := range warnings { 219 fmt.Fprintf(ui.Err, "%s\n", ui.TranslateText(warning)) 220 } 221 } 222 223 // DisplayError outputs the translated error message to ui.Err if the error 224 // satisfies TranslatableError, otherwise it outputs the original error message 225 // to ui.Err. It also outputs "FAILED" in bold red to ui.Out. 226 func (ui *UI) DisplayError(err error) { 227 var errMsg string 228 if translatableError, ok := err.(TranslatableError); ok { 229 errMsg = translatableError.Translate(ui.translate) 230 } else { 231 errMsg = err.Error() 232 } 233 fmt.Fprintf(ui.Err, "%s\n", errMsg) 234 fmt.Fprintf(ui.Out, "%s\n", ui.addFlavor(ui.TranslateText("FAILED"), red, true)) 235 } 236 237 const LogTimestampFormat = "2006-01-02T15:04:05.00-0700" 238 239 // DisplayLogMessage formats and outputs a given log message. 240 func (ui *UI) DisplayLogMessage(message LogMessage, displayHeader bool) { 241 var header string 242 if displayHeader { 243 time := message.Timestamp().In(ui.TimezoneLocation).Format(LogTimestampFormat) 244 245 header = fmt.Sprintf("%s [%s/%s] %s ", 246 time, 247 message.SourceType(), 248 message.SourceInstance(), 249 message.Type(), 250 ) 251 } 252 253 for _, line := range strings.Split(message.Message(), "\n") { 254 logLine := fmt.Sprintf("%s%s", header, strings.TrimRight(line, "\r\n")) 255 if message.Type() == "ERR" { 256 logLine = ui.addFlavor(logLine, red, false) 257 } 258 fmt.Fprintf(ui.Out, "%s\n", logLine) 259 } 260 } 261 262 // addFlavor adds the provided text color and bold style to the text. 263 func (ui *UI) addFlavor(text string, textColor color.Attribute, isBold bool) string { 264 if len(text) == 0 { 265 return text 266 } 267 268 colorPrinter := color.New(textColor) 269 270 switch ui.colorEnabled { 271 case configv3.ColorEnabled: 272 colorPrinter.EnableColor() 273 case configv3.ColorDisabled: 274 colorPrinter.DisableColor() 275 } 276 277 if isBold { 278 colorPrinter = colorPrinter.Add(color.Bold) 279 } 280 281 return colorPrinter.SprintFunc()(text) 282 } 283 284 // getFirstSet returns the first map if 1 or more maps are provided. Otherwise 285 // it returns the empty map. 286 func getFirstSet(list []map[string]interface{}) map[string]interface{} { 287 if list == nil || len(list) == 0 { 288 return map[string]interface{}{} 289 } 290 return list[0] 291 }