github.com/dcarley/cf-cli@v6.24.1-0.20170220111324-4225ff346898+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  	"time"
    14  
    15  	"code.cloudfoundry.org/cli/util/configv3"
    16  	"github.com/fatih/color"
    17  	runewidth "github.com/mattn/go-runewidth"
    18  	"github.com/nicksnyder/go-i18n/i18n"
    19  	"github.com/vito/go-interact/interact"
    20  )
    21  
    22  const (
    23  	red   color.Attribute = color.FgRed
    24  	green                 = color.FgGreen
    25  	// yellow                         = color.FgYellow
    26  	// magenta                        = color.FgMagenta
    27  	cyan           = color.FgCyan
    28  	white          = color.FgWhite
    29  	defaultFgColor = 38
    30  )
    31  
    32  //go:generate counterfeiter . Config
    33  
    34  // Config is the UI configuration
    35  type Config interface {
    36  	// ColorEnabled enables or disabled color
    37  	ColorEnabled() configv3.ColorSetting
    38  	// Locale is the language to translate the output to
    39  	Locale() string
    40  }
    41  
    42  //go:generate counterfeiter . TranslatableError
    43  
    44  // TranslatableError it wraps the error interface adding a way to set the
    45  // translation function on the error
    46  type TranslatableError interface {
    47  	// Returns back the untranslated error string
    48  	Error() string
    49  	Translate(func(string, ...interface{}) string) string
    50  }
    51  
    52  //go:generate counterfeiter . LogMessage
    53  
    54  // LogMessage is a log response representing one to many joined lines of a log
    55  // message.
    56  type LogMessage interface {
    57  	Message() string
    58  	Type() string
    59  	Timestamp() time.Time
    60  	SourceType() string
    61  	SourceInstance() string
    62  }
    63  
    64  // UI is interface to interact with the user
    65  type UI struct {
    66  	// In is the input buffer
    67  	In io.Reader
    68  	// Out is the output buffer
    69  	Out io.Writer
    70  	// Err is the error buffer
    71  	Err io.Writer
    72  
    73  	colorEnabled configv3.ColorSetting
    74  	translate    i18n.TranslateFunc
    75  
    76  	TimezoneLocation *time.Location
    77  }
    78  
    79  // NewUI will return a UI object where Out is set to STDOUT, In is set to
    80  // STDIN, and Err is set to STDERR
    81  func NewUI(c Config) (*UI, error) {
    82  	translateFunc, err := GetTranslationFunc(c)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	location := time.Now().Location()
    88  
    89  	return &UI{
    90  		In:               os.Stdin,
    91  		Out:              color.Output,
    92  		Err:              os.Stderr,
    93  		colorEnabled:     c.ColorEnabled(),
    94  		translate:        translateFunc,
    95  		TimezoneLocation: location,
    96  	}, nil
    97  }
    98  
    99  // NewTestUI will return a UI object where Out, In, and Err are customizable,
   100  // and colors are disabled
   101  func NewTestUI(in io.Reader, out io.Writer, err io.Writer) *UI {
   102  	return &UI{
   103  		In:           in,
   104  		Out:          out,
   105  		Err:          err,
   106  		colorEnabled: configv3.ColorDisabled,
   107  		translate:    translationWrapper(i18n.IdentityTfunc()),
   108  	}
   109  }
   110  
   111  // TranslateText passes the template through an internationalization function
   112  // to translate it to a pre-configured language, and returns the template with
   113  // templateValues substituted in. Only the first map in templateValues is used.
   114  func (ui *UI) TranslateText(template string, templateValues ...map[string]interface{}) string {
   115  	return ui.translate(template, getFirstSet(templateValues))
   116  }
   117  
   118  // UserFriendlyDate converts the time to UTC and then formats it to ISO8601.
   119  func (ui *UI) UserFriendlyDate(input time.Time) string {
   120  	return input.UTC().Format(time.RFC3339)
   121  }
   122  
   123  // DisplayOK outputs a bold green translated "OK" to UI.Out.
   124  func (ui *UI) DisplayOK() {
   125  	fmt.Fprintf(ui.Out, "%s\n", ui.addFlavor(ui.TranslateText("OK"), green, true))
   126  }
   127  
   128  // DisplayNewline outputs a newline to UI.Out.
   129  func (ui *UI) DisplayNewline() {
   130  	fmt.Fprintf(ui.Out, "\n")
   131  }
   132  
   133  // DisplayBoolPrompt outputs the prompt and waits for user input. It only
   134  // allows for a boolean response. A default boolean response can be set with
   135  // defaultResponse.
   136  func (ui *UI) DisplayBoolPrompt(prompt string, defaultResponse bool) (bool, error) {
   137  	response := defaultResponse
   138  	interactivePrompt := interact.NewInteraction(fmt.Sprintf("%s%s", prompt, ui.addFlavor(">>", cyan, true)))
   139  	interactivePrompt.Input = ui.In
   140  	interactivePrompt.Output = ui.Out
   141  	err := interactivePrompt.Resolve(&response)
   142  	return response, err
   143  }
   144  
   145  // DisplayTable outputs a matrix of strings as a table to UI.Out. Prefix will
   146  // be prepended to each row and padding adds the specified number of spaces
   147  // between columns.
   148  func (ui *UI) DisplayTable(prefix string, table [][]string, padding int) {
   149  	if len(table) == 0 {
   150  		return
   151  	}
   152  
   153  	var columnPadding []int
   154  
   155  	rows := len(table)
   156  	columns := len(table[0])
   157  	for col := 0; col < columns; col++ {
   158  		var max int
   159  		for row := 0; row < rows; row++ {
   160  			if strLen := runewidth.StringWidth(table[row][col]); max < strLen {
   161  				max = strLen
   162  			}
   163  		}
   164  		columnPadding = append(columnPadding, max+padding)
   165  	}
   166  
   167  	for row := 0; row < rows; row++ {
   168  		fmt.Fprintf(ui.Out, prefix)
   169  		for col := 0; col < columns; col++ {
   170  			var addedPadding int
   171  			if col+1 != columns {
   172  				addedPadding = columnPadding[col] - runewidth.StringWidth(table[row][col])
   173  			}
   174  			fmt.Fprintf(ui.Out, "%s%s", table[row][col], strings.Repeat(" ", addedPadding))
   175  		}
   176  		fmt.Fprintf(ui.Out, "\n")
   177  	}
   178  }
   179  
   180  // DisplayText translates the template, substitutes in templateValues, and
   181  // outputs the result to ui.Out. Only the first map in templateValues is used.
   182  func (ui *UI) DisplayText(template string, templateValues ...map[string]interface{}) {
   183  	fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, templateValues...))
   184  }
   185  
   186  // DisplayPair translates the attribute, translates the template, substitutes
   187  // templateValues into the template, and outputs the pair to ui.Out. Only the
   188  // first map in templateValues is used.
   189  func (ui *UI) DisplayPair(attribute string, template string, templateValues ...map[string]interface{}) {
   190  	fmt.Fprintf(ui.Out, "%s: %s\n", ui.TranslateText(attribute), ui.TranslateText(template, templateValues...))
   191  }
   192  
   193  // DisplayHeader translates the header, bolds and adds the default color to the
   194  // header, and outputs the result to ui.Out.
   195  func (ui *UI) DisplayHeader(text string) {
   196  	fmt.Fprintf(ui.Out, "%s\n", ui.addFlavor(ui.TranslateText(text), defaultFgColor, true))
   197  }
   198  
   199  // DisplayTextWithFlavor translates the template, bolds and adds cyan color to
   200  // templateValues, substitutes templateValues into the template, and outputs
   201  // the result to ui.Out. Only the first map in templateValues is used.
   202  func (ui *UI) DisplayTextWithFlavor(template string, templateValues ...map[string]interface{}) {
   203  	firstTemplateValues := getFirstSet(templateValues)
   204  	for key, value := range firstTemplateValues {
   205  		firstTemplateValues[key] = ui.addFlavor(fmt.Sprint(value), cyan, true)
   206  	}
   207  	fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues))
   208  }
   209  
   210  // DisplayWarning translates the warning, substitutes in templateValues, and
   211  // outputs to ui.Err. Only the first map in templateValues is used.
   212  func (ui *UI) DisplayWarning(template string, templateValues ...map[string]interface{}) {
   213  	fmt.Fprintf(ui.Err, "%s\n", ui.TranslateText(template, templateValues...))
   214  }
   215  
   216  // DisplayWarnings translates the warnings and outputs to ui.Err.
   217  func (ui *UI) DisplayWarnings(warnings []string) {
   218  	for _, warning := range warnings {
   219  		fmt.Fprintf(ui.Err, "%s\n", ui.TranslateText(warning))
   220  	}
   221  }
   222  
   223  // DisplayError outputs the translated error message to ui.Err if the error
   224  // satisfies TranslatableError, otherwise it outputs the original error message
   225  // to ui.Err. It also outputs "FAILED" in bold red to ui.Out.
   226  func (ui *UI) DisplayError(err error) {
   227  	var errMsg string
   228  	if translatableError, ok := err.(TranslatableError); ok {
   229  		errMsg = translatableError.Translate(ui.translate)
   230  	} else {
   231  		errMsg = err.Error()
   232  	}
   233  	fmt.Fprintf(ui.Err, "%s\n", errMsg)
   234  	fmt.Fprintf(ui.Out, "%s\n", ui.addFlavor(ui.TranslateText("FAILED"), red, true))
   235  }
   236  
   237  const LogTimestampFormat = "2006-01-02T15:04:05.00-0700"
   238  
   239  // DisplayLogMessage formats and outputs a given log message.
   240  func (ui *UI) DisplayLogMessage(message LogMessage, displayHeader bool) {
   241  	var header string
   242  	if displayHeader {
   243  		time := message.Timestamp().In(ui.TimezoneLocation).Format(LogTimestampFormat)
   244  
   245  		header = fmt.Sprintf("%s [%s/%s] %s ",
   246  			time,
   247  			message.SourceType(),
   248  			message.SourceInstance(),
   249  			message.Type(),
   250  		)
   251  	}
   252  
   253  	for _, line := range strings.Split(message.Message(), "\n") {
   254  		logLine := fmt.Sprintf("%s%s", header, strings.TrimRight(line, "\r\n"))
   255  		if message.Type() == "ERR" {
   256  			logLine = ui.addFlavor(logLine, red, false)
   257  		}
   258  		fmt.Fprintf(ui.Out, "%s\n", logLine)
   259  	}
   260  }
   261  
   262  // addFlavor adds the provided text color and bold style to the text.
   263  func (ui *UI) addFlavor(text string, textColor color.Attribute, isBold bool) string {
   264  	if len(text) == 0 {
   265  		return text
   266  	}
   267  
   268  	colorPrinter := color.New(textColor)
   269  
   270  	switch ui.colorEnabled {
   271  	case configv3.ColorEnabled:
   272  		colorPrinter.EnableColor()
   273  	case configv3.ColorDisabled:
   274  		colorPrinter.DisableColor()
   275  	}
   276  
   277  	if isBold {
   278  		colorPrinter = colorPrinter.Add(color.Bold)
   279  	}
   280  
   281  	return colorPrinter.SprintFunc()(text)
   282  }
   283  
   284  // getFirstSet returns the first map if 1 or more maps are provided. Otherwise
   285  // it returns the empty map.
   286  func getFirstSet(list []map[string]interface{}) map[string]interface{} {
   287  	if list == nil || len(list) == 0 {
   288  		return map[string]interface{}{}
   289  	}
   290  	return list[0]
   291  }