github.com/cloudfoundry/cli@v7.1.0+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 runewidth "github.com/mattn/go-runewidth" 20 "github.com/vito/go-interact/interact" 21 ) 22 23 var realExiter exiterFunc = os.Exit 24 25 var realInteract interactorFunc = func(prompt string, choices ...interact.Choice) Resolver { 26 return &interactionWrapper{ 27 interact.NewInteraction(prompt, choices...), 28 } 29 } 30 31 //go:generate counterfeiter . Interactor 32 33 // Interactor hides interact.NewInteraction for testing purposes 34 type Interactor interface { 35 NewInteraction(prompt string, choices ...interact.Choice) Resolver 36 } 37 38 type interactorFunc func(prompt string, choices ...interact.Choice) Resolver 39 40 func (f interactorFunc) NewInteraction(prompt string, choices ...interact.Choice) Resolver { 41 return f(prompt, choices...) 42 } 43 44 type interactionWrapper struct { 45 interact.Interaction 46 } 47 48 func (w *interactionWrapper) SetIn(in io.Reader) { 49 w.Input = in 50 } 51 52 func (w *interactionWrapper) SetOut(o io.Writer) { 53 w.Output = o 54 } 55 56 //go:generate counterfeiter . Exiter 57 58 // Exiter hides os.Exit for testing purposes 59 type Exiter interface { 60 Exit(code int) 61 } 62 63 type exiterFunc func(int) 64 65 func (f exiterFunc) Exit(code int) { 66 f(code) 67 } 68 69 // UI is interface to interact with the user 70 type UI struct { 71 // In is the input buffer 72 In io.Reader 73 // Out is the output buffer 74 Out io.Writer 75 // OutForInteration is the output buffer when working with go-interact. When 76 // working with Windows, color.Output does not work with TTY detection. So 77 // real STDOUT is required or go-interact will not properly work. 78 OutForInteration io.Writer 79 // Err is the error buffer 80 Err io.Writer 81 82 colorEnabled configv3.ColorSetting 83 translate TranslateFunc 84 Exiter Exiter 85 86 terminalLock *sync.Mutex 87 fileLock *sync.Mutex 88 89 Interactor Interactor 90 91 IsTTY bool 92 TerminalWidth int 93 94 TimezoneLocation *time.Location 95 96 deferred []string 97 } 98 99 // NewUI will return a UI object where Out is set to STDOUT, In is set to 100 // STDIN, and Err is set to STDERR 101 func NewUI(config Config) (*UI, error) { 102 translateFunc, err := GetTranslationFunc(config) 103 if err != nil { 104 return nil, err 105 } 106 107 location := time.Now().Location() 108 109 return &UI{ 110 In: os.Stdin, 111 Out: color.Output, 112 OutForInteration: os.Stdout, 113 Err: os.Stderr, 114 colorEnabled: config.ColorEnabled(), 115 translate: translateFunc, 116 terminalLock: &sync.Mutex{}, 117 Exiter: realExiter, 118 fileLock: &sync.Mutex{}, 119 Interactor: realInteract, 120 IsTTY: config.IsTTY(), 121 TerminalWidth: config.TerminalWidth(), 122 TimezoneLocation: location, 123 }, nil 124 } 125 126 // NewPluginUI will return a UI object where OUT and ERR are customizable. 127 func NewPluginUI(config Config, outBuffer io.Writer, errBuffer io.Writer) (*UI, error) { 128 translateFunc, translationError := GetTranslationFunc(config) 129 if translationError != nil { 130 return nil, translationError 131 } 132 133 location := time.Now().Location() 134 135 return &UI{ 136 In: nil, 137 Out: outBuffer, 138 OutForInteration: outBuffer, 139 Err: errBuffer, 140 colorEnabled: configv3.ColorDisabled, 141 translate: translateFunc, 142 terminalLock: &sync.Mutex{}, 143 Exiter: realExiter, 144 fileLock: &sync.Mutex{}, 145 Interactor: realInteract, 146 IsTTY: config.IsTTY(), 147 TerminalWidth: config.TerminalWidth(), 148 TimezoneLocation: location, 149 }, nil 150 } 151 152 // NewTestUI will return a UI object where Out, In, and Err are customizable, 153 // and colors are disabled 154 func NewTestUI(in io.Reader, out io.Writer, err io.Writer) *UI { 155 translationFunc, translateErr := generateTranslationFunc([]byte("[]")) 156 if translateErr != nil { 157 panic(translateErr) 158 } 159 160 return &UI{ 161 In: in, 162 Out: out, 163 OutForInteration: out, 164 Err: err, 165 Exiter: realExiter, 166 colorEnabled: configv3.ColorDisabled, 167 translate: translationFunc, 168 Interactor: realInteract, 169 terminalLock: &sync.Mutex{}, 170 fileLock: &sync.Mutex{}, 171 TimezoneLocation: time.UTC, 172 } 173 } 174 175 // DeferText translates the template, substitutes in templateValues, and 176 // Enqueues the output to be presented later via FlushDeferred. Only the first 177 // map in templateValues is used. 178 func (ui *UI) DeferText(template string, templateValues ...map[string]interface{}) { 179 s := fmt.Sprintf("%s\n", ui.TranslateText(template, templateValues...)) 180 ui.deferred = append(ui.deferred, s) 181 } 182 183 func (ui *UI) DisplayDeprecationWarning() { 184 ui.terminalLock.Lock() 185 defer ui.terminalLock.Unlock() 186 187 fmt.Fprintf(ui.Err, "Deprecation warning: This command has been deprecated. This feature will be removed in the future.\n") 188 } 189 190 // DisplayError outputs the translated error message to ui.Err if the error 191 // satisfies TranslatableError, otherwise it outputs the original error message 192 // to ui.Err. It also outputs "FAILED" in bold red to ui.Out. 193 func (ui *UI) DisplayError(err error) { 194 var errMsg string 195 if translatableError, ok := err.(translatableerror.TranslatableError); ok { 196 errMsg = translatableError.Translate(ui.translate) 197 } else { 198 errMsg = err.Error() 199 } 200 fmt.Fprintf(ui.Err, "%s\n", errMsg) 201 202 ui.terminalLock.Lock() 203 defer ui.terminalLock.Unlock() 204 205 fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText("FAILED"), color.New(color.FgRed, color.Bold))) 206 } 207 208 func (ui *UI) DisplayFileDeprecationWarning() { 209 ui.terminalLock.Lock() 210 defer ui.terminalLock.Unlock() 211 212 fmt.Fprintf(ui.Err, "Deprecation warning: This command has been deprecated and will be removed in the future. For similar functionality, please use the `cf ssh` command instead.\n") 213 } 214 215 // DisplayHeader translates the header, bolds and adds the default color to the 216 // header, and outputs the result to ui.Out. 217 func (ui *UI) DisplayHeader(text string) { 218 ui.terminalLock.Lock() 219 defer ui.terminalLock.Unlock() 220 221 fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText(text), color.New(color.Bold))) 222 } 223 224 // DisplayNewline outputs a newline to UI.Out. 225 func (ui *UI) DisplayNewline() { 226 ui.terminalLock.Lock() 227 defer ui.terminalLock.Unlock() 228 229 fmt.Fprintf(ui.Out, "\n") 230 } 231 232 // DisplayOK outputs a bold green translated "OK" to UI.Out. 233 func (ui *UI) DisplayOK() { 234 ui.terminalLock.Lock() 235 defer ui.terminalLock.Unlock() 236 237 fmt.Fprintf(ui.Out, "%s\n\n", ui.modifyColor(ui.TranslateText("OK"), color.New(color.FgGreen, color.Bold))) 238 } 239 240 // DisplayText translates the template, substitutes in templateValues, and 241 // outputs the result to ui.Out. Only the first map in templateValues is used. 242 func (ui *UI) DisplayText(template string, templateValues ...map[string]interface{}) { 243 ui.terminalLock.Lock() 244 defer ui.terminalLock.Unlock() 245 246 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, templateValues...)) 247 } 248 249 // DisplayTextWithBold translates the template, bolds the templateValues, 250 // substitutes templateValues into the template, and outputs 251 // the result to ui.Out. Only the first map in templateValues is used. 252 func (ui *UI) DisplayTextWithBold(template string, templateValues ...map[string]interface{}) { 253 ui.terminalLock.Lock() 254 defer ui.terminalLock.Unlock() 255 256 firstTemplateValues := getFirstSet(templateValues) 257 for key, value := range firstTemplateValues { 258 firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.Bold)) 259 } 260 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues)) 261 } 262 263 // DisplayTextWithFlavor translates the template, bolds and adds cyan color to 264 // templateValues, substitutes templateValues into the template, and outputs 265 // the result to ui.Out. Only the first map in templateValues is used. 266 func (ui *UI) DisplayTextWithFlavor(template string, templateValues ...map[string]interface{}) { 267 ui.terminalLock.Lock() 268 defer ui.terminalLock.Unlock() 269 270 firstTemplateValues := getFirstSet(templateValues) 271 for key, value := range firstTemplateValues { 272 firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.FgCyan, color.Bold)) 273 } 274 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues)) 275 } 276 277 // FlushDeferred displays text previously deferred (using DeferText) to the UI's 278 // `Out`. 279 func (ui *UI) FlushDeferred() { 280 ui.terminalLock.Lock() 281 defer ui.terminalLock.Unlock() 282 283 for _, s := range ui.deferred { 284 fmt.Fprint(ui.Out, s) 285 } 286 ui.deferred = []string{} 287 } 288 289 // GetErr returns the error writer. 290 func (ui *UI) GetErr() io.Writer { 291 return ui.Err 292 } 293 294 // GetIn returns the input reader. 295 func (ui *UI) GetIn() io.Reader { 296 return ui.In 297 } 298 299 // GetOut returns the output writer. Same as `Writer`. 300 func (ui *UI) GetOut() io.Writer { 301 return ui.Out 302 } 303 304 // TranslateText passes the template through an internationalization function 305 // to translate it to a pre-configured language, and returns the template with 306 // templateValues substituted in. Only the first map in templateValues is used. 307 func (ui *UI) TranslateText(template string, templateValues ...map[string]interface{}) string { 308 return ui.translate(template, getFirstSet(templateValues)) 309 } 310 311 // UserFriendlyDate converts the time to UTC and then formats it to ISO8601. 312 func (ui *UI) UserFriendlyDate(input time.Time) string { 313 return input.Local().Format("Mon 02 Jan 15:04:05 MST 2006") 314 } 315 316 // Writer returns the output writer. Same as `GetOut`. 317 func (ui *UI) Writer() io.Writer { 318 return ui.Out 319 } 320 321 func (ui *UI) displayWrappingTableWithWidth(prefix string, table [][]string, padding int) { 322 ui.terminalLock.Lock() 323 defer ui.terminalLock.Unlock() 324 325 var columnPadding []int 326 327 rows := len(table) 328 columns := len(table[0]) 329 330 for col := 0; col < columns-1; col++ { 331 var max int 332 for row := 0; row < rows; row++ { 333 if strLen := runewidth.StringWidth(table[row][col]); max < strLen { 334 max = strLen 335 } 336 } 337 columnPadding = append(columnPadding, max+padding) 338 } 339 340 spilloverPadding := len(prefix) + sum(columnPadding) 341 lastColumnWidth := ui.TerminalWidth - spilloverPadding 342 343 for row := 0; row < rows; row++ { 344 fmt.Fprint(ui.Out, prefix) 345 346 // for all columns except last, add cell value and padding 347 for col := 0; col < columns-1; col++ { 348 var addedPadding int 349 if col+1 != columns { 350 addedPadding = columnPadding[col] - runewidth.StringWidth(table[row][col]) 351 } 352 fmt.Fprintf(ui.Out, "%s%s", table[row][col], strings.Repeat(" ", addedPadding)) 353 } 354 355 // 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 356 words := strings.Split(table[row][columns-1], " ") 357 currentWidth := 0 358 359 for _, word := range words { 360 wordWidth := runewidth.StringWidth(word) 361 switch { 362 case currentWidth == 0: 363 currentWidth = wordWidth 364 fmt.Fprintf(ui.Out, "%s", word) 365 case (wordWidth + 1 + currentWidth) > lastColumnWidth: 366 fmt.Fprintf(ui.Out, "\n%s%s", strings.Repeat(" ", spilloverPadding), word) 367 currentWidth = wordWidth 368 default: 369 fmt.Fprintf(ui.Out, " %s", word) 370 currentWidth += wordWidth + 1 371 } 372 } 373 374 fmt.Fprintf(ui.Out, "\n") 375 } 376 } 377 378 func (ui *UI) modifyColor(text string, colorPrinter *color.Color) string { 379 if len(text) == 0 { 380 return text 381 } 382 383 switch ui.colorEnabled { 384 case configv3.ColorEnabled: 385 colorPrinter.EnableColor() 386 case configv3.ColorDisabled: 387 colorPrinter.DisableColor() 388 } 389 390 return colorPrinter.SprintFunc()(text) 391 } 392 393 // getFirstSet returns the first map if 1 or more maps are provided. Otherwise 394 // it returns the empty map. 395 func getFirstSet(list []map[string]interface{}) map[string]interface{} { 396 if len(list) == 0 { 397 return map[string]interface{}{} 398 } 399 return list[0] 400 } 401 402 func sum(intSlice []int) int { 403 sum := 0 404 405 for _, i := range intSlice { 406 sum += i 407 } 408 409 return sum 410 }