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  }