github.com/randomtask1155/cli@v6.41.1-0.20181227003417-a98eed78cbde+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 ) 21 22 // UI is interface to interact with the user 23 type UI struct { 24 // In is the input buffer 25 In io.Reader 26 // Out is the output buffer 27 Out io.Writer 28 // OutForInteration is the output buffer when working with go-interact. When 29 // working with Windows, color.Output does not work with TTY detection. So 30 // real STDOUT is required or go-interact will not properly work. 31 OutForInteration io.Writer 32 // Err is the error buffer 33 Err io.Writer 34 35 colorEnabled configv3.ColorSetting 36 translate TranslateFunc 37 38 terminalLock *sync.Mutex 39 fileLock *sync.Mutex 40 41 IsTTY bool 42 TerminalWidth int 43 44 TimezoneLocation *time.Location 45 } 46 47 // NewUI will return a UI object where Out is set to STDOUT, In is set to 48 // STDIN, and Err is set to STDERR 49 func NewUI(config Config) (*UI, error) { 50 translateFunc, err := GetTranslationFunc(config) 51 if err != nil { 52 return nil, err 53 } 54 55 location := time.Now().Location() 56 57 return &UI{ 58 In: os.Stdin, 59 Out: color.Output, 60 OutForInteration: os.Stdout, 61 Err: os.Stderr, 62 colorEnabled: config.ColorEnabled(), 63 translate: translateFunc, 64 terminalLock: &sync.Mutex{}, 65 fileLock: &sync.Mutex{}, 66 IsTTY: config.IsTTY(), 67 TerminalWidth: config.TerminalWidth(), 68 TimezoneLocation: location, 69 }, nil 70 } 71 72 // NewTestUI will return a UI object where Out, In, and Err are customizable, 73 // and colors are disabled 74 func NewTestUI(in io.Reader, out io.Writer, err io.Writer) *UI { 75 translationFunc, translateErr := generateTranslationFunc([]byte("[]")) 76 if translateErr != nil { 77 panic(translateErr) 78 } 79 80 return &UI{ 81 In: in, 82 Out: out, 83 OutForInteration: out, 84 Err: err, 85 colorEnabled: configv3.ColorDisabled, 86 translate: translationFunc, 87 terminalLock: &sync.Mutex{}, 88 fileLock: &sync.Mutex{}, 89 TimezoneLocation: time.UTC, 90 } 91 } 92 93 // DisplayError outputs the translated error message to ui.Err if the error 94 // satisfies TranslatableError, otherwise it outputs the original error message 95 // to ui.Err. It also outputs "FAILED" in bold red to ui.Out. 96 func (ui *UI) DisplayError(err error) { 97 var errMsg string 98 if translatableError, ok := err.(translatableerror.TranslatableError); ok { 99 errMsg = translatableError.Translate(ui.translate) 100 } else { 101 errMsg = err.Error() 102 } 103 fmt.Fprintf(ui.Err, "%s\n", errMsg) 104 105 ui.terminalLock.Lock() 106 defer ui.terminalLock.Unlock() 107 108 fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText("FAILED"), color.New(color.FgRed, color.Bold))) 109 } 110 111 // DisplayHeader translates the header, bolds and adds the default color to the 112 // header, and outputs the result to ui.Out. 113 func (ui *UI) DisplayHeader(text string) { 114 ui.terminalLock.Lock() 115 defer ui.terminalLock.Unlock() 116 117 fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText(text), color.New(color.Bold))) 118 } 119 120 // DisplayNewline outputs a newline to UI.Out. 121 func (ui *UI) DisplayNewline() { 122 ui.terminalLock.Lock() 123 defer ui.terminalLock.Unlock() 124 125 fmt.Fprintf(ui.Out, "\n") 126 } 127 128 // DisplayOK outputs a bold green translated "OK" to UI.Out. 129 func (ui *UI) DisplayOK() { 130 ui.terminalLock.Lock() 131 defer ui.terminalLock.Unlock() 132 133 fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText("OK"), color.New(color.FgGreen, color.Bold))) 134 } 135 136 // DisplayText translates the template, substitutes in templateValues, and 137 // outputs the result to ui.Out. Only the first map in templateValues is used. 138 func (ui *UI) DisplayText(template string, templateValues ...map[string]interface{}) { 139 ui.terminalLock.Lock() 140 defer ui.terminalLock.Unlock() 141 142 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, templateValues...)) 143 } 144 145 // DisplayTextWithBold translates the template, bolds the templateValues, 146 // substitutes templateValues into the template, and outputs 147 // the result to ui.Out. Only the first map in templateValues is used. 148 func (ui *UI) DisplayTextWithBold(template string, templateValues ...map[string]interface{}) { 149 ui.terminalLock.Lock() 150 defer ui.terminalLock.Unlock() 151 152 firstTemplateValues := getFirstSet(templateValues) 153 for key, value := range firstTemplateValues { 154 firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.Bold)) 155 } 156 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues)) 157 } 158 159 // DisplayTextWithFlavor translates the template, bolds and adds cyan color to 160 // templateValues, substitutes templateValues into the template, and outputs 161 // the result to ui.Out. Only the first map in templateValues is used. 162 func (ui *UI) DisplayTextWithFlavor(template string, templateValues ...map[string]interface{}) { 163 ui.terminalLock.Lock() 164 defer ui.terminalLock.Unlock() 165 166 firstTemplateValues := getFirstSet(templateValues) 167 for key, value := range firstTemplateValues { 168 firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.FgCyan, color.Bold)) 169 } 170 fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues)) 171 } 172 173 // DisplayWarning translates the warning, substitutes in templateValues, and 174 // outputs to ui.Err. Only the first map in templateValues is used. 175 func (ui *UI) DisplayWarning(template string, templateValues ...map[string]interface{}) { 176 fmt.Fprintf(ui.Err, "%s\n\n", ui.TranslateText(template, templateValues...)) 177 } 178 179 // DisplayWarnings translates the warnings and outputs to ui.Err. 180 func (ui *UI) DisplayWarnings(warnings []string) { 181 for _, warning := range warnings { 182 fmt.Fprintf(ui.Err, "%s\n", ui.TranslateText(warning)) 183 } 184 if len(warnings) > 0 { 185 fmt.Fprintln(ui.Err) 186 } 187 } 188 189 // GetErr returns the error writer. 190 func (ui *UI) GetErr() io.Writer { 191 return ui.Err 192 } 193 194 // GetIn returns the input reader. 195 func (ui *UI) GetIn() io.Reader { 196 return ui.In 197 } 198 199 // GetOut returns the output writer. Same as `Writer`. 200 func (ui *UI) GetOut() io.Writer { 201 return ui.Out 202 } 203 204 // TranslateText passes the template through an internationalization function 205 // to translate it to a pre-configured language, and returns the template with 206 // templateValues substituted in. Only the first map in templateValues is used. 207 func (ui *UI) TranslateText(template string, templateValues ...map[string]interface{}) string { 208 return ui.translate(template, getFirstSet(templateValues)) 209 } 210 211 // UserFriendlyDate converts the time to UTC and then formats it to ISO8601. 212 func (ui *UI) UserFriendlyDate(input time.Time) string { 213 return input.Local().Format("Mon 02 Jan 15:04:05 MST 2006") 214 } 215 216 // Writer returns the output writer. Same as `GetOut`. 217 func (ui *UI) Writer() io.Writer { 218 return ui.Out 219 } 220 221 func (ui *UI) displayWrappingTableWithWidth(prefix string, table [][]string, padding int) { 222 ui.terminalLock.Lock() 223 defer ui.terminalLock.Unlock() 224 225 var columnPadding []int 226 227 rows := len(table) 228 columns := len(table[0]) 229 230 for col := 0; col < columns-1; col++ { 231 var max int 232 for row := 0; row < rows; row++ { 233 if strLen := runewidth.StringWidth(table[row][col]); max < strLen { 234 max = strLen 235 } 236 } 237 columnPadding = append(columnPadding, max+padding) 238 } 239 240 spilloverPadding := len(prefix) + sum(columnPadding) 241 lastColumnWidth := ui.TerminalWidth - spilloverPadding 242 243 for row := 0; row < rows; row++ { 244 fmt.Fprintf(ui.Out, prefix) 245 246 // for all columns except last, add cell value and padding 247 for col := 0; col < columns-1; col++ { 248 var addedPadding int 249 if col+1 != columns { 250 addedPadding = columnPadding[col] - runewidth.StringWidth(table[row][col]) 251 } 252 fmt.Fprintf(ui.Out, "%s%s", table[row][col], strings.Repeat(" ", addedPadding)) 253 } 254 255 // 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 256 words := strings.Split(table[row][columns-1], " ") 257 currentWidth := 0 258 259 for _, word := range words { 260 wordWidth := runewidth.StringWidth(word) 261 if currentWidth == 0 { 262 currentWidth = wordWidth 263 fmt.Fprintf(ui.Out, "%s", word) 264 } else if wordWidth+1+currentWidth > lastColumnWidth { 265 fmt.Fprintf(ui.Out, "\n%s%s", strings.Repeat(" ", spilloverPadding), word) 266 currentWidth = wordWidth 267 } else { 268 fmt.Fprintf(ui.Out, " %s", word) 269 currentWidth += wordWidth + 1 270 } 271 } 272 273 fmt.Fprintf(ui.Out, "\n") 274 } 275 } 276 277 func (ui *UI) modifyColor(text string, colorPrinter *color.Color) string { 278 if len(text) == 0 { 279 return text 280 } 281 282 switch ui.colorEnabled { 283 case configv3.ColorEnabled: 284 colorPrinter.EnableColor() 285 case configv3.ColorDisabled: 286 colorPrinter.DisableColor() 287 } 288 289 return colorPrinter.SprintFunc()(text) 290 } 291 292 // getFirstSet returns the first map if 1 or more maps are provided. Otherwise 293 // it returns the empty map. 294 func getFirstSet(list []map[string]interface{}) map[string]interface{} { 295 if list == nil || len(list) == 0 { 296 return map[string]interface{}{} 297 } 298 return list[0] 299 } 300 301 func sum(intSlice []int) int { 302 sum := 0 303 304 for _, i := range intSlice { 305 sum += i 306 } 307 308 return sum 309 }