github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+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 "sync" 14 "time" 15 16 "code.cloudfoundry.org/cli/command/translatableerror" 17 "code.cloudfoundry.org/cli/util/configv3" 18 "github.com/fatih/color" 19 "github.com/lunixbochs/vtclean" 20 runewidth "github.com/mattn/go-runewidth" 21 "github.com/vito/go-interact/interact" 22 ) 23 24 // LogTimestampFormat is the timestamp formatting for log lines. 25 const LogTimestampFormat = "2006-01-02T15:04:05.00-0700" 26 27 // DefaultTableSpacePadding is the default space padding in tables. 28 const DefaultTableSpacePadding = 3 29 30 //go:generate counterfeiter . Config 31 32 // Config is the UI configuration. 33 type Config interface { 34 // ColorEnabled enables or disabled color 35 ColorEnabled() configv3.ColorSetting 36 // Locale is the language to translate the output to 37 Locale() string 38 // IsTTY returns true when the ui has a TTY 39 IsTTY() bool 40 // TerminalWidth returns the width of the terminal 41 TerminalWidth() int 42 } 43 44 //go:generate counterfeiter . LogMessage 45 46 // LogMessage is a log response representing one to many joined lines of a log 47 // message. 48 type LogMessage interface { 49 Message() string 50 Type() string 51 Timestamp() time.Time 52 SourceType() string 53 SourceInstance() string 54 } 55 56 // UI is interface to interact with the user 57 type UI struct { 58 // In is the input buffer 59 In io.Reader 60 // Out is the output buffer 61 Out io.Writer 62 // OutForInteration is the output buffer when working with go-interact. When 63 // working with Windows, color.Output does not work with TTY detection. So 64 // real STDOUT is required or go-interact will not properly work. 65 OutForInteration io.Writer 66 // Err is the error buffer 67 Err io.Writer 68 69 colorEnabled configv3.ColorSetting 70 translate TranslateFunc 71 72 terminalLock *sync.Mutex 73 fileLock *sync.Mutex 74 75 IsTTY bool 76 TerminalWidth int 77 78 TimezoneLocation *time.Location 79 } 80 81 // NewUI will return a UI object where Out is set to STDOUT, In is set to 82 // STDIN, and Err is set to STDERR 83 func NewUI(config Config) (*UI, error) { 84 translateFunc, err := GetTranslationFunc(config) 85 if err != nil { 86 return nil, err 87 } 88 89 location := time.Now().Location() 90 91 return &UI{ 92 In: os.Stdin, 93 Out: color.Output, 94 OutForInteration: os.Stdout, 95 Err: os.Stderr, 96 colorEnabled: config.ColorEnabled(), 97 translate: translateFunc, 98 terminalLock: &sync.Mutex{}, 99 fileLock: &sync.Mutex{}, 100 IsTTY: config.IsTTY(), 101 TerminalWidth: config.TerminalWidth(), 102 TimezoneLocation: location, 103 }, nil 104 } 105 106 // NewTestUI will return a UI object where Out, In, and Err are customizable, 107 // and colors are disabled 108 func NewTestUI(in io.Reader, out io.Writer, err io.Writer) *UI { 109 translationFunc, translateErr := generateTranslationFunc([]byte("[]")) 110 if translateErr != nil { 111 panic(translateErr) 112 } 113 114 return &UI{ 115 In: in, 116 Out: out, 117 OutForInteration: out, 118 Err: err, 119 colorEnabled: configv3.ColorDisabled, 120 translate: translationFunc, 121 terminalLock: &sync.Mutex{}, 122 fileLock: &sync.Mutex{}, 123 TimezoneLocation: time.UTC, 124 } 125 } 126 127 func (ui *UI) GetIn() io.Reader { 128 return ui.In 129 } 130 131 func (ui *UI) GetOut() io.Writer { 132 return ui.Out 133 } 134 135 func (ui *UI) GetErr() io.Writer { 136 return ui.Err 137 } 138 139 // DisplayBoolPrompt outputs the prompt and waits for user input. It only 140 // allows for a boolean response. A default boolean response can be set with 141 // defaultResponse. 142 func (ui *UI) DisplayBoolPrompt(defaultResponse bool, template string, templateValues ...map[string]interface{}) (bool, error) { 143 ui.terminalLock.Lock() 144 defer ui.terminalLock.Unlock() 145 146 response := defaultResponse 147 interactivePrompt := interact.NewInteraction(ui.TranslateText(template, templateValues...)) 148 interactivePrompt.Input = ui.In 149 interactivePrompt.Output = ui.OutForInteration 150 err := interactivePrompt.Resolve(&response) 151 return response, err 152 } 153 154 // DisplayPasswordPrompt outputs the prompt and waits for user input. Hides 155 // user's response from the screen. 156 func (ui *UI) DisplayPasswordPrompt(template string, templateValues ...map[string]interface{}) (string, error) { 157 ui.terminalLock.Lock() 158 defer ui.terminalLock.Unlock() 159 160 var password interact.Password 161 interactivePrompt := interact.NewInteraction(ui.TranslateText(template, templateValues...)) 162 interactivePrompt.Input = ui.In 163 interactivePrompt.Output = ui.OutForInteration 164 err := interactivePrompt.Resolve(interact.Required(&password)) 165 return string(password), err 166 } 167 168 // DisplayError outputs the translated error message to ui.Err if the error 169 // satisfies TranslatableError, otherwise it outputs the original error message 170 // to ui.Err. It also outputs "FAILED" in bold red to ui.Out. 171 func (ui *UI) DisplayError(err error) { 172 var errMsg string 173 if translatableError, ok := err.(translatableerror.TranslatableError); ok { 174 errMsg = translatableError.Translate(ui.translate) 175 } else { 176 errMsg = err.Error() 177 } 178 fmt.Fprintf(ui.Err, "%s\n", errMsg) 179 180 ui.terminalLock.Lock() 181 defer ui.terminalLock.Unlock() 182 183 fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText("FAILED"), color.New(color.FgRed, color.Bold))) 184 } 185 186 // DisplayHeader translates the header, bolds and adds the default color to the 187 // header, and outputs the result to ui.Out. 188 func (ui *UI) DisplayHeader(text string) { 189 ui.terminalLock.Lock() 190 defer ui.terminalLock.Unlock() 191 192 fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText(text), color.New(color.Bold))) 193 } 194 195 // DisplayKeyValueTable outputs a matrix of strings as a table to UI.Out. 196 // Prefix will be prepended to each row and padding adds the specified number 197 // of spaces between columns. The final columns may wrap to multiple lines but 198 // will still be confined to the last column. Wrapping will occur on word 199 // boundaries. 200 func (ui *UI) DisplayKeyValueTable(prefix string, table [][]string, padding int) { 201 rows := len(table) 202 if rows == 0 { 203 return 204 } 205 206 columns := len(table[0]) 207 208 if columns < 2 || !ui.IsTTY { 209 ui.DisplayNonWrappingTable(prefix, table, padding) 210 return 211 } 212 213 ui.displayWrappingTableWithWidth(prefix, table, padding) 214 } 215 216 // DisplayLogMessage formats and outputs a given log message. 217 func (ui *UI) DisplayLogMessage(message LogMessage, displayHeader bool) { 218 ui.terminalLock.Lock() 219 defer ui.terminalLock.Unlock() 220 221 var header string 222 if displayHeader { 223 time := message.Timestamp().In(ui.TimezoneLocation).Format(LogTimestampFormat) 224 225 header = fmt.Sprintf("%s [%s/%s] %s ", 226 time, 227 message.SourceType(), 228 message.SourceInstance(), 229 message.Type(), 230 ) 231 } 232 233 for _, line := range strings.Split(message.Message(), "\n") { 234 logLine := fmt.Sprintf("%s%s", header, strings.TrimRight(line, "\r\n")) 235 if message.Type() == "ERR" { 236 logLine = ui.modifyColor(logLine, color.New(color.FgRed)) 237 } 238 fmt.Fprintf(ui.Out, " %s\n", logLine) 239 } 240 } 241 242 // DisplayNewline outputs a newline to UI.Out. 243 func (ui *UI) DisplayNewline() { 244 ui.terminalLock.Lock() 245 defer ui.terminalLock.Unlock() 246 247 fmt.Fprintf(ui.Out, "\n") 248 } 249 250 // DisplayNonWrappingTable outputs a matrix of strings as a table to UI.Out. Prefix will 251 // be prepended to each row and padding adds the specified number of spaces 252 // between columns. 253 func (ui *UI) DisplayNonWrappingTable(prefix string, table [][]string, padding int) { 254 ui.terminalLock.Lock() 255 defer ui.terminalLock.Unlock() 256 257 if len(table) == 0 { 258 return 259 } 260 261 var columnPadding []int 262 263 rows := len(table) 264 columns := len(table[0]) 265 for col := 0; col < columns; col++ { 266 var max int 267 for row := 0; row < rows; row++ { 268 if strLen := wordSize(table[row][col]); max < strLen { 269 max = strLen 270 } 271 } 272 columnPadding = append(columnPadding, max+padding) 273 } 274 275 for row := 0; row < rows; row++ { 276 fmt.Fprintf(ui.Out, prefix) 277 for col := 0; col < columns; col++ { 278 data := table[row][col] 279 var addedPadding int 280 if col+1 != columns { 281 addedPadding = columnPadding[col] - wordSize(data) 282 } 283 fmt.Fprintf(ui.Out, "%s%s", data, strings.Repeat(" ", addedPadding)) 284 } 285 fmt.Fprintf(ui.Out, "\n") 286 } 287 } 288 289 // DisplayOK outputs a bold green translated "OK" to UI.Out. 290 func (ui *UI) DisplayOK() { 291 ui.terminalLock.Lock() 292 defer ui.terminalLock.Unlock() 293 294 fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText("OK"), color.New(color.FgGreen, color.Bold))) 295 } 296 297 func (ui *UI) DisplayTableWithHeader(prefix string, table [][]string, padding int) { 298 if len(table) == 0 { 299 return 300 } 301 for i, str := range table[0] { 302 table[0][i] = ui.modifyColor(str, color.New(color.Bold)) 303 } 304 305 ui.DisplayNonWrappingTable(prefix, table, padding) 306 } 307 308 // DisplayText translates the template, substitutes in templateValues, and 309 // outputs the result to ui.Out. Only the first map in templateValues is used. 310 func (ui *UI) DisplayText(template string, templateValues ...map[string]interface{}) { 311 ui.terminalLock.Lock() 312 defer ui.terminalLock.Unlock() 313 314 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, templateValues...)) 315 } 316 317 // DisplayTextWithFlavor translates the template, bolds and adds cyan color to 318 // templateValues, substitutes templateValues into the template, and outputs 319 // the result to ui.Out. Only the first map in templateValues is used. 320 func (ui *UI) DisplayTextWithFlavor(template string, templateValues ...map[string]interface{}) { 321 ui.terminalLock.Lock() 322 defer ui.terminalLock.Unlock() 323 324 firstTemplateValues := getFirstSet(templateValues) 325 for key, value := range firstTemplateValues { 326 firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.FgCyan, color.Bold)) 327 } 328 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues)) 329 } 330 331 // DisplayTextWithBold translates the template, bolds the templateValues, 332 // substitutes templateValues into the template, and outputs 333 // the result to ui.Out. Only the first map in templateValues is used. 334 func (ui *UI) DisplayTextWithBold(template string, templateValues ...map[string]interface{}) { 335 ui.terminalLock.Lock() 336 defer ui.terminalLock.Unlock() 337 338 firstTemplateValues := getFirstSet(templateValues) 339 for key, value := range firstTemplateValues { 340 firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.Bold)) 341 } 342 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues)) 343 } 344 345 // DisplayWarning translates the warning, substitutes in templateValues, and 346 // outputs to ui.Err. Only the first map in templateValues is used. 347 func (ui *UI) DisplayWarning(template string, templateValues ...map[string]interface{}) { 348 fmt.Fprintf(ui.Err, "%s\n", ui.TranslateText(template, templateValues...)) 349 } 350 351 // DisplayWarnings translates the warnings and outputs to ui.Err. 352 func (ui *UI) DisplayWarnings(warnings []string) { 353 for _, warning := range warnings { 354 fmt.Fprintf(ui.Err, "%s\n", ui.TranslateText(warning)) 355 } 356 } 357 358 // RequestLoggerFileWriter returns a RequestLoggerFileWriter that cannot 359 // overwrite another RequestLoggerFileWriter. 360 func (ui *UI) RequestLoggerFileWriter(filePaths []string) *RequestLoggerFileWriter { 361 return newRequestLoggerFileWriter(ui, ui.fileLock, filePaths) 362 } 363 364 // RequestLoggerTerminalDisplay returns a RequestLoggerTerminalDisplay that 365 // cannot overwrite another RequestLoggerTerminalDisplay or the current 366 // display. 367 func (ui *UI) RequestLoggerTerminalDisplay() *RequestLoggerTerminalDisplay { 368 return newRequestLoggerTerminalDisplay(ui, ui.terminalLock) 369 } 370 371 // TranslateText passes the template through an internationalization function 372 // to translate it to a pre-configured language, and returns the template with 373 // templateValues substituted in. Only the first map in templateValues is used. 374 func (ui *UI) TranslateText(template string, templateValues ...map[string]interface{}) string { 375 return ui.translate(template, getFirstSet(templateValues)) 376 } 377 378 // UserFriendlyDate converts the time to UTC and then formats it to ISO8601. 379 func (ui *UI) UserFriendlyDate(input time.Time) string { 380 return input.Local().Format("Mon 02 Jan 15:04:05 MST 2006") 381 } 382 383 func (ui *UI) Writer() io.Writer { 384 return ui.Out 385 } 386 387 func (ui *UI) displayWrappingTableWithWidth(prefix string, table [][]string, padding int) { 388 ui.terminalLock.Lock() 389 defer ui.terminalLock.Unlock() 390 391 var columnPadding []int 392 393 rows := len(table) 394 columns := len(table[0]) 395 396 for col := 0; col < columns-1; col++ { 397 var max int 398 for row := 0; row < rows; row++ { 399 if strLen := runewidth.StringWidth(table[row][col]); max < strLen { 400 max = strLen 401 } 402 } 403 columnPadding = append(columnPadding, max+padding) 404 } 405 406 spilloverPadding := len(prefix) + sum(columnPadding) 407 lastColumnWidth := ui.TerminalWidth - spilloverPadding 408 409 for row := 0; row < rows; row++ { 410 fmt.Fprintf(ui.Out, prefix) 411 412 // for all columns except last, add cell value and padding 413 for col := 0; col < columns-1; col++ { 414 var addedPadding int 415 if col+1 != columns { 416 addedPadding = columnPadding[col] - runewidth.StringWidth(table[row][col]) 417 } 418 fmt.Fprintf(ui.Out, "%s%s", table[row][col], strings.Repeat(" ", addedPadding)) 419 } 420 421 // for last column, add each word individually. If the added word would make the column exceed terminal width, create a new line and add padding 422 words := strings.Split(table[row][columns-1], " ") 423 currentWidth := 0 424 425 for _, word := range words { 426 wordWidth := runewidth.StringWidth(word) 427 if currentWidth == 0 { 428 currentWidth = wordWidth 429 fmt.Fprintf(ui.Out, "%s", word) 430 } else if wordWidth+1+currentWidth > lastColumnWidth { 431 fmt.Fprintf(ui.Out, "\n%s%s", strings.Repeat(" ", spilloverPadding), word) 432 currentWidth = wordWidth 433 } else { 434 fmt.Fprintf(ui.Out, " %s", word) 435 currentWidth += wordWidth + 1 436 } 437 } 438 439 fmt.Fprintf(ui.Out, "\n") 440 } 441 } 442 443 // getFirstSet returns the first map if 1 or more maps are provided. Otherwise 444 // it returns the empty map. 445 func getFirstSet(list []map[string]interface{}) map[string]interface{} { 446 if list == nil || len(list) == 0 { 447 return map[string]interface{}{} 448 } 449 return list[0] 450 } 451 452 func (ui *UI) modifyColor(text string, colorPrinter *color.Color) string { 453 if len(text) == 0 { 454 return text 455 } 456 457 switch ui.colorEnabled { 458 case configv3.ColorEnabled: 459 colorPrinter.EnableColor() 460 case configv3.ColorDisabled: 461 colorPrinter.DisableColor() 462 } 463 464 return colorPrinter.SprintFunc()(text) 465 } 466 467 func sum(intSlice []int) int { 468 sum := 0 469 470 for _, i := range intSlice { 471 sum += i 472 } 473 474 return sum 475 } 476 477 func wordSize(str string) int { 478 cleanStr := vtclean.Clean(str, false) 479 return runewidth.StringWidth(cleanStr) 480 }