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