github.com/liamawhite/cli-with-i18n@v6.32.1-0.20171122084555-dede0a5c3448+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  	"github.com/fatih/color"
    17  	"github.com/liamawhite/cli-with-i18n/command/translatableerror"
    18  	"github.com/liamawhite/cli-with-i18n/util/configv3"
    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  	// Err is the error buffer
    63  	Err io.Writer
    64  
    65  	colorEnabled configv3.ColorSetting
    66  	translate    TranslateFunc
    67  
    68  	terminalLock *sync.Mutex
    69  	fileLock     *sync.Mutex
    70  
    71  	IsTTY         bool
    72  	TerminalWidth int
    73  
    74  	TimezoneLocation *time.Location
    75  }
    76  
    77  // NewUI will return a UI object where Out is set to STDOUT, In is set to
    78  // STDIN, and Err is set to STDERR
    79  func NewUI(config Config) (*UI, error) {
    80  	translateFunc, err := GetTranslationFunc(config)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	location := time.Now().Location()
    86  
    87  	return &UI{
    88  		In:               os.Stdin,
    89  		Out:              color.Output,
    90  		Err:              os.Stderr,
    91  		colorEnabled:     config.ColorEnabled(),
    92  		translate:        translateFunc,
    93  		terminalLock:     &sync.Mutex{},
    94  		fileLock:         &sync.Mutex{},
    95  		IsTTY:            config.IsTTY(),
    96  		TerminalWidth:    config.TerminalWidth(),
    97  		TimezoneLocation: location,
    98  	}, nil
    99  }
   100  
   101  // NewTestUI will return a UI object where Out, In, and Err are customizable,
   102  // and colors are disabled
   103  func NewTestUI(in io.Reader, out io.Writer, err io.Writer) *UI {
   104  	translationFunc, translateErr := generateTranslationFunc([]byte("[]"))
   105  	if translateErr != nil {
   106  		panic(translateErr)
   107  	}
   108  
   109  	return &UI{
   110  		In:               in,
   111  		Out:              out,
   112  		Err:              err,
   113  		colorEnabled:     configv3.ColorDisabled,
   114  		translate:        translationFunc,
   115  		terminalLock:     &sync.Mutex{},
   116  		fileLock:         &sync.Mutex{},
   117  		TimezoneLocation: time.UTC,
   118  	}
   119  }
   120  
   121  // DisplayBoolPrompt outputs the prompt and waits for user input. It only
   122  // allows for a boolean response. A default boolean response can be set with
   123  // defaultResponse.
   124  func (ui *UI) DisplayBoolPrompt(defaultResponse bool, template string, templateValues ...map[string]interface{}) (bool, error) {
   125  	ui.terminalLock.Lock()
   126  	defer ui.terminalLock.Unlock()
   127  
   128  	response := defaultResponse
   129  	interactivePrompt := interact.NewInteraction(ui.TranslateText(template, templateValues...))
   130  	interactivePrompt.Input = ui.In
   131  	interactivePrompt.Output = ui.Out
   132  	err := interactivePrompt.Resolve(&response)
   133  	return response, err
   134  }
   135  
   136  // DisplayError outputs the translated error message to ui.Err if the error
   137  // satisfies TranslatableError, otherwise it outputs the original error message
   138  // to ui.Err. It also outputs "FAILED" in bold red to ui.Out.
   139  func (ui *UI) DisplayError(err error) {
   140  	var errMsg string
   141  	if translatableError, ok := err.(translatableerror.TranslatableError); ok {
   142  		errMsg = translatableError.Translate(ui.translate)
   143  	} else {
   144  		errMsg = err.Error()
   145  	}
   146  	fmt.Fprintf(ui.Err, "%s\n", errMsg)
   147  
   148  	ui.terminalLock.Lock()
   149  	defer ui.terminalLock.Unlock()
   150  
   151  	fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText("FAILED"), color.New(color.FgRed, color.Bold)))
   152  }
   153  
   154  // DisplayHeader translates the header, bolds and adds the default color to the
   155  // header, and outputs the result to ui.Out.
   156  func (ui *UI) DisplayHeader(text string) {
   157  	ui.terminalLock.Lock()
   158  	defer ui.terminalLock.Unlock()
   159  
   160  	fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText(text), color.New(color.Bold)))
   161  }
   162  
   163  // DisplayKeyValueTable outputs a matrix of strings as a table to UI.Out.
   164  // Prefix will be prepended to each row and padding adds the specified number
   165  // of spaces between columns. The final columns may wrap to multiple lines but
   166  // will still be confined to the last column. Wrapping will occur on word
   167  // boundaries.
   168  func (ui *UI) DisplayKeyValueTable(prefix string, table [][]string, padding int) {
   169  	rows := len(table)
   170  	if rows == 0 {
   171  		return
   172  	}
   173  
   174  	columns := len(table[0])
   175  
   176  	if columns < 2 || !ui.IsTTY {
   177  		ui.DisplayNonWrappingTable(prefix, table, padding)
   178  		return
   179  	}
   180  
   181  	ui.displayWrappingTableWithWidth(prefix, table, padding)
   182  }
   183  
   184  // DisplayLogMessage formats and outputs a given log message.
   185  func (ui *UI) DisplayLogMessage(message LogMessage, displayHeader bool) {
   186  	ui.terminalLock.Lock()
   187  	defer ui.terminalLock.Unlock()
   188  
   189  	var header string
   190  	if displayHeader {
   191  		time := message.Timestamp().In(ui.TimezoneLocation).Format(LogTimestampFormat)
   192  
   193  		header = fmt.Sprintf("%s [%s/%s] %s ",
   194  			time,
   195  			message.SourceType(),
   196  			message.SourceInstance(),
   197  			message.Type(),
   198  		)
   199  	}
   200  
   201  	for _, line := range strings.Split(message.Message(), "\n") {
   202  		logLine := fmt.Sprintf("%s%s", header, strings.TrimRight(line, "\r\n"))
   203  		if message.Type() == "ERR" {
   204  			logLine = ui.modifyColor(logLine, color.New(color.FgRed))
   205  		}
   206  		fmt.Fprintf(ui.Out, "   %s\n", logLine)
   207  	}
   208  }
   209  
   210  // DisplayNewline outputs a newline to UI.Out.
   211  func (ui *UI) DisplayNewline() {
   212  	ui.terminalLock.Lock()
   213  	defer ui.terminalLock.Unlock()
   214  
   215  	fmt.Fprintf(ui.Out, "\n")
   216  }
   217  
   218  // DisplayNonWrappingTable outputs a matrix of strings as a table to UI.Out. Prefix will
   219  // be prepended to each row and padding adds the specified number of spaces
   220  // between columns.
   221  func (ui *UI) DisplayNonWrappingTable(prefix string, table [][]string, padding int) {
   222  	ui.terminalLock.Lock()
   223  	defer ui.terminalLock.Unlock()
   224  
   225  	if len(table) == 0 {
   226  		return
   227  	}
   228  
   229  	var columnPadding []int
   230  
   231  	rows := len(table)
   232  	columns := len(table[0])
   233  	for col := 0; col < columns; col++ {
   234  		var max int
   235  		for row := 0; row < rows; row++ {
   236  			if strLen := wordSize(table[row][col]); max < strLen {
   237  				max = strLen
   238  			}
   239  		}
   240  		columnPadding = append(columnPadding, max+padding)
   241  	}
   242  
   243  	for row := 0; row < rows; row++ {
   244  		fmt.Fprintf(ui.Out, prefix)
   245  		for col := 0; col < columns; col++ {
   246  			data := table[row][col]
   247  			var addedPadding int
   248  			if col+1 != columns {
   249  				addedPadding = columnPadding[col] - wordSize(data)
   250  			}
   251  			fmt.Fprintf(ui.Out, "%s%s", data, strings.Repeat(" ", addedPadding))
   252  		}
   253  		fmt.Fprintf(ui.Out, "\n")
   254  	}
   255  }
   256  
   257  // DisplayOK outputs a bold green translated "OK" to UI.Out.
   258  func (ui *UI) DisplayOK() {
   259  	ui.terminalLock.Lock()
   260  	defer ui.terminalLock.Unlock()
   261  
   262  	fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText("OK"), color.New(color.FgGreen, color.Bold)))
   263  }
   264  
   265  func (ui *UI) DisplayTableWithHeader(prefix string, table [][]string, padding int) {
   266  	if len(table) == 0 {
   267  		return
   268  	}
   269  	for i, str := range table[0] {
   270  		table[0][i] = ui.modifyColor(str, color.New(color.Bold))
   271  	}
   272  
   273  	ui.DisplayNonWrappingTable(prefix, table, padding)
   274  }
   275  
   276  // DisplayText translates the template, substitutes in templateValues, and
   277  // outputs the result to ui.Out. Only the first map in templateValues is used.
   278  func (ui *UI) DisplayText(template string, templateValues ...map[string]interface{}) {
   279  	ui.terminalLock.Lock()
   280  	defer ui.terminalLock.Unlock()
   281  
   282  	fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, templateValues...))
   283  }
   284  
   285  // DisplayTextWithFlavor translates the template, bolds and adds cyan color to
   286  // templateValues, substitutes templateValues into the template, and outputs
   287  // the result to ui.Out. Only the first map in templateValues is used.
   288  func (ui *UI) DisplayTextWithFlavor(template string, templateValues ...map[string]interface{}) {
   289  	ui.terminalLock.Lock()
   290  	defer ui.terminalLock.Unlock()
   291  
   292  	firstTemplateValues := getFirstSet(templateValues)
   293  	for key, value := range firstTemplateValues {
   294  		firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.FgCyan, color.Bold))
   295  	}
   296  	fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues))
   297  }
   298  
   299  // DisplayTextWithBold translates the template, bolds the templateValues,
   300  // substitutes templateValues into the template, and outputs
   301  // the result to ui.Out. Only the first map in templateValues is used.
   302  func (ui *UI) DisplayTextWithBold(template string, templateValues ...map[string]interface{}) {
   303  	ui.terminalLock.Lock()
   304  	defer ui.terminalLock.Unlock()
   305  
   306  	firstTemplateValues := getFirstSet(templateValues)
   307  	for key, value := range firstTemplateValues {
   308  		firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.Bold))
   309  	}
   310  	fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues))
   311  }
   312  
   313  // DisplayWarning translates the warning, substitutes in templateValues, and
   314  // outputs to ui.Err. Only the first map in templateValues is used.
   315  func (ui *UI) DisplayWarning(template string, templateValues ...map[string]interface{}) {
   316  	fmt.Fprintf(ui.Err, "%s\n", ui.TranslateText(template, templateValues...))
   317  }
   318  
   319  // DisplayWarnings translates the warnings and outputs to ui.Err.
   320  func (ui *UI) DisplayWarnings(warnings []string) {
   321  	for _, warning := range warnings {
   322  		fmt.Fprintf(ui.Err, "%s\n", ui.TranslateText(warning))
   323  	}
   324  }
   325  
   326  // RequestLoggerFileWriter returns a RequestLoggerFileWriter that cannot
   327  // overwrite another RequestLoggerFileWriter.
   328  func (ui *UI) RequestLoggerFileWriter(filePaths []string) *RequestLoggerFileWriter {
   329  	return newRequestLoggerFileWriter(ui, ui.fileLock, filePaths)
   330  }
   331  
   332  // RequestLoggerTerminalDisplay returns a RequestLoggerTerminalDisplay that
   333  // cannot overwrite another RequestLoggerTerminalDisplay or the current
   334  // display.
   335  func (ui *UI) RequestLoggerTerminalDisplay() *RequestLoggerTerminalDisplay {
   336  	return newRequestLoggerTerminalDisplay(ui, ui.terminalLock)
   337  }
   338  
   339  // TranslateText passes the template through an internationalization function
   340  // to translate it to a pre-configured language, and returns the template with
   341  // templateValues substituted in. Only the first map in templateValues is used.
   342  func (ui *UI) TranslateText(template string, templateValues ...map[string]interface{}) string {
   343  	return ui.translate(template, getFirstSet(templateValues))
   344  }
   345  
   346  // UserFriendlyDate converts the time to UTC and then formats it to ISO8601.
   347  func (ui *UI) UserFriendlyDate(input time.Time) string {
   348  	return input.Local().Format("Mon 02 Jan 15:04:05 MST 2006")
   349  }
   350  
   351  func (ui *UI) Writer() io.Writer {
   352  	return ui.Out
   353  }
   354  
   355  func (ui *UI) displayWrappingTableWithWidth(prefix string, table [][]string, padding int) {
   356  	ui.terminalLock.Lock()
   357  	defer ui.terminalLock.Unlock()
   358  
   359  	var columnPadding []int
   360  
   361  	rows := len(table)
   362  	columns := len(table[0])
   363  
   364  	for col := 0; col < columns-1; col++ {
   365  		var max int
   366  		for row := 0; row < rows; row++ {
   367  			if strLen := runewidth.StringWidth(table[row][col]); max < strLen {
   368  				max = strLen
   369  			}
   370  		}
   371  		columnPadding = append(columnPadding, max+padding)
   372  	}
   373  
   374  	spilloverPadding := len(prefix) + sum(columnPadding)
   375  	lastColumnWidth := ui.TerminalWidth - spilloverPadding
   376  
   377  	for row := 0; row < rows; row++ {
   378  		fmt.Fprintf(ui.Out, prefix)
   379  
   380  		// for all columns except last, add cell value and padding
   381  		for col := 0; col < columns-1; col++ {
   382  			var addedPadding int
   383  			if col+1 != columns {
   384  				addedPadding = columnPadding[col] - runewidth.StringWidth(table[row][col])
   385  			}
   386  			fmt.Fprintf(ui.Out, "%s%s", table[row][col], strings.Repeat(" ", addedPadding))
   387  		}
   388  
   389  		// 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
   390  		words := strings.Split(table[row][columns-1], " ")
   391  		currentWidth := 0
   392  
   393  		for _, word := range words {
   394  			wordWidth := runewidth.StringWidth(word)
   395  			if currentWidth == 0 {
   396  				currentWidth = wordWidth
   397  				fmt.Fprintf(ui.Out, "%s", word)
   398  			} else if wordWidth+1+currentWidth > lastColumnWidth {
   399  				fmt.Fprintf(ui.Out, "\n%s%s", strings.Repeat(" ", spilloverPadding), word)
   400  				currentWidth = wordWidth
   401  			} else {
   402  				fmt.Fprintf(ui.Out, " %s", word)
   403  				currentWidth += wordWidth + 1
   404  			}
   405  		}
   406  
   407  		fmt.Fprintf(ui.Out, "\n")
   408  	}
   409  }
   410  
   411  // getFirstSet returns the first map if 1 or more maps are provided. Otherwise
   412  // it returns the empty map.
   413  func getFirstSet(list []map[string]interface{}) map[string]interface{} {
   414  	if list == nil || len(list) == 0 {
   415  		return map[string]interface{}{}
   416  	}
   417  	return list[0]
   418  }
   419  
   420  func (ui *UI) modifyColor(text string, colorPrinter *color.Color) string {
   421  	if len(text) == 0 {
   422  		return text
   423  	}
   424  
   425  	switch ui.colorEnabled {
   426  	case configv3.ColorEnabled:
   427  		colorPrinter.EnableColor()
   428  	case configv3.ColorDisabled:
   429  		colorPrinter.DisableColor()
   430  	}
   431  
   432  	return colorPrinter.SprintFunc()(text)
   433  }
   434  
   435  func sum(intSlice []int) int {
   436  	sum := 0
   437  
   438  	for _, i := range intSlice {
   439  		sum += i
   440  	}
   441  
   442  	return sum
   443  }
   444  
   445  func wordSize(str string) int {
   446  	cleanStr := vtclean.Clean(str, false)
   447  	return runewidth.StringWidth(cleanStr)
   448  }