github.com/mook-as/cf-cli@v7.0.0-beta.28.0.20200120190804-b91c115fae48+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 // NewTestUI will return a UI object where Out, In, and Err are customizable, 127 // and colors are disabled 128 func NewTestUI(in io.Reader, out io.Writer, err io.Writer) *UI { 129 translationFunc, translateErr := generateTranslationFunc([]byte("[]")) 130 if translateErr != nil { 131 panic(translateErr) 132 } 133 134 return &UI{ 135 In: in, 136 Out: out, 137 OutForInteration: out, 138 Err: err, 139 Exiter: realExiter, 140 colorEnabled: configv3.ColorDisabled, 141 translate: translationFunc, 142 Interactor: realInteract, 143 terminalLock: &sync.Mutex{}, 144 fileLock: &sync.Mutex{}, 145 TimezoneLocation: time.UTC, 146 } 147 } 148 149 // DeferText translates the template, substitutes in templateValues, and 150 // Enqueues the output to be presented later via FlushDeferred. Only the first 151 // map in templateValues is used. 152 func (ui *UI) DeferText(template string, templateValues ...map[string]interface{}) { 153 s := fmt.Sprintf("%s\n", ui.TranslateText(template, templateValues...)) 154 ui.deferred = append(ui.deferred, s) 155 } 156 157 func (ui *UI) DisplayDeprecationWarning() { 158 ui.terminalLock.Lock() 159 defer ui.terminalLock.Unlock() 160 161 fmt.Fprintf(ui.Err, "Deprecation warning: This command has been deprecated. This feature will be removed in the future.\n") 162 } 163 164 // DisplayError outputs the translated error message to ui.Err if the error 165 // satisfies TranslatableError, otherwise it outputs the original error message 166 // to ui.Err. It also outputs "FAILED" in bold red to ui.Out. 167 func (ui *UI) DisplayError(err error) { 168 var errMsg string 169 if translatableError, ok := err.(translatableerror.TranslatableError); ok { 170 errMsg = translatableError.Translate(ui.translate) 171 } else { 172 errMsg = err.Error() 173 } 174 fmt.Fprintf(ui.Err, "%s\n", errMsg) 175 176 ui.terminalLock.Lock() 177 defer ui.terminalLock.Unlock() 178 179 fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText("FAILED"), color.New(color.FgRed, color.Bold))) 180 } 181 182 func (ui *UI) DisplayFileDeprecationWarning() { 183 ui.terminalLock.Lock() 184 defer ui.terminalLock.Unlock() 185 186 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") 187 } 188 189 // DisplayHeader translates the header, bolds and adds the default color to the 190 // header, and outputs the result to ui.Out. 191 func (ui *UI) DisplayHeader(text string) { 192 ui.terminalLock.Lock() 193 defer ui.terminalLock.Unlock() 194 195 fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText(text), color.New(color.Bold))) 196 } 197 198 // DisplayNewline outputs a newline to UI.Out. 199 func (ui *UI) DisplayNewline() { 200 ui.terminalLock.Lock() 201 defer ui.terminalLock.Unlock() 202 203 fmt.Fprintf(ui.Out, "\n") 204 } 205 206 // DisplayOK outputs a bold green translated "OK" to UI.Out. 207 func (ui *UI) DisplayOK() { 208 ui.terminalLock.Lock() 209 defer ui.terminalLock.Unlock() 210 211 fmt.Fprintf(ui.Out, "%s\n\n", ui.modifyColor(ui.TranslateText("OK"), color.New(color.FgGreen, color.Bold))) 212 } 213 214 // DisplayText translates the template, substitutes in templateValues, and 215 // outputs the result to ui.Out. Only the first map in templateValues is used. 216 func (ui *UI) DisplayText(template string, templateValues ...map[string]interface{}) { 217 ui.terminalLock.Lock() 218 defer ui.terminalLock.Unlock() 219 220 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, templateValues...)) 221 } 222 223 // DisplayTextWithBold translates the template, bolds the templateValues, 224 // substitutes templateValues into the template, and outputs 225 // the result to ui.Out. Only the first map in templateValues is used. 226 func (ui *UI) DisplayTextWithBold(template string, templateValues ...map[string]interface{}) { 227 ui.terminalLock.Lock() 228 defer ui.terminalLock.Unlock() 229 230 firstTemplateValues := getFirstSet(templateValues) 231 for key, value := range firstTemplateValues { 232 firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.Bold)) 233 } 234 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues)) 235 } 236 237 // DisplayTextWithFlavor translates the template, bolds and adds cyan color to 238 // templateValues, substitutes templateValues into the template, and outputs 239 // the result to ui.Out. Only the first map in templateValues is used. 240 func (ui *UI) DisplayTextWithFlavor(template string, templateValues ...map[string]interface{}) { 241 ui.terminalLock.Lock() 242 defer ui.terminalLock.Unlock() 243 244 firstTemplateValues := getFirstSet(templateValues) 245 for key, value := range firstTemplateValues { 246 firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.FgCyan, color.Bold)) 247 } 248 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues)) 249 } 250 251 // FlushDeferred displays text previously deferred (using DeferText) to the UI's 252 // `Out`. 253 func (ui *UI) FlushDeferred() { 254 ui.terminalLock.Lock() 255 defer ui.terminalLock.Unlock() 256 257 for _, s := range ui.deferred { 258 fmt.Fprint(ui.Out, s) 259 } 260 ui.deferred = []string{} 261 } 262 263 // GetErr returns the error writer. 264 func (ui *UI) GetErr() io.Writer { 265 return ui.Err 266 } 267 268 // GetIn returns the input reader. 269 func (ui *UI) GetIn() io.Reader { 270 return ui.In 271 } 272 273 // GetOut returns the output writer. Same as `Writer`. 274 func (ui *UI) GetOut() io.Writer { 275 return ui.Out 276 } 277 278 // TranslateText passes the template through an internationalization function 279 // to translate it to a pre-configured language, and returns the template with 280 // templateValues substituted in. Only the first map in templateValues is used. 281 func (ui *UI) TranslateText(template string, templateValues ...map[string]interface{}) string { 282 return ui.translate(template, getFirstSet(templateValues)) 283 } 284 285 // UserFriendlyDate converts the time to UTC and then formats it to ISO8601. 286 func (ui *UI) UserFriendlyDate(input time.Time) string { 287 return input.Local().Format("Mon 02 Jan 15:04:05 MST 2006") 288 } 289 290 // Writer returns the output writer. Same as `GetOut`. 291 func (ui *UI) Writer() io.Writer { 292 return ui.Out 293 } 294 295 func (ui *UI) displayWrappingTableWithWidth(prefix string, table [][]string, padding int) { 296 ui.terminalLock.Lock() 297 defer ui.terminalLock.Unlock() 298 299 var columnPadding []int 300 301 rows := len(table) 302 columns := len(table[0]) 303 304 for col := 0; col < columns-1; col++ { 305 var max int 306 for row := 0; row < rows; row++ { 307 if strLen := runewidth.StringWidth(table[row][col]); max < strLen { 308 max = strLen 309 } 310 } 311 columnPadding = append(columnPadding, max+padding) 312 } 313 314 spilloverPadding := len(prefix) + sum(columnPadding) 315 lastColumnWidth := ui.TerminalWidth - spilloverPadding 316 317 for row := 0; row < rows; row++ { 318 fmt.Fprint(ui.Out, prefix) 319 320 // for all columns except last, add cell value and padding 321 for col := 0; col < columns-1; col++ { 322 var addedPadding int 323 if col+1 != columns { 324 addedPadding = columnPadding[col] - runewidth.StringWidth(table[row][col]) 325 } 326 fmt.Fprintf(ui.Out, "%s%s", table[row][col], strings.Repeat(" ", addedPadding)) 327 } 328 329 // 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 330 words := strings.Split(table[row][columns-1], " ") 331 currentWidth := 0 332 333 for _, word := range words { 334 wordWidth := runewidth.StringWidth(word) 335 switch { 336 case currentWidth == 0: 337 currentWidth = wordWidth 338 fmt.Fprintf(ui.Out, "%s", word) 339 case (wordWidth + 1 + currentWidth) > lastColumnWidth: 340 fmt.Fprintf(ui.Out, "\n%s%s", strings.Repeat(" ", spilloverPadding), word) 341 currentWidth = wordWidth 342 default: 343 fmt.Fprintf(ui.Out, " %s", word) 344 currentWidth += wordWidth + 1 345 } 346 } 347 348 fmt.Fprintf(ui.Out, "\n") 349 } 350 } 351 352 func (ui *UI) modifyColor(text string, colorPrinter *color.Color) string { 353 if len(text) == 0 { 354 return text 355 } 356 357 switch ui.colorEnabled { 358 case configv3.ColorEnabled: 359 colorPrinter.EnableColor() 360 case configv3.ColorDisabled: 361 colorPrinter.DisableColor() 362 } 363 364 return colorPrinter.SprintFunc()(text) 365 } 366 367 // getFirstSet returns the first map if 1 or more maps are provided. Otherwise 368 // it returns the empty map. 369 func getFirstSet(list []map[string]interface{}) map[string]interface{} { 370 if len(list) == 0 { 371 return map[string]interface{}{} 372 } 373 return list[0] 374 } 375 376 func sum(intSlice []int) int { 377 sum := 0 378 379 for _, i := range intSlice { 380 sum += i 381 } 382 383 return sum 384 }