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  }