github.com/cloudfoundry/cli@v7.1.0+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  	"github.com/vito/go-interact/interact"
    21  )
    22  
    23  var realExiter exiterFunc = os.Exit
    24  
    25  var realInteract interactorFunc = func(prompt string, choices ...interact.Choice) Resolver {
    26  	return &interactionWrapper{
    27  		interact.NewInteraction(prompt, choices...),
    28  	}
    29  }
    30  
    31  //go:generate counterfeiter . Interactor
    32  
    33  // Interactor hides interact.NewInteraction for testing purposes
    34  type Interactor interface {
    35  	NewInteraction(prompt string, choices ...interact.Choice) Resolver
    36  }
    37  
    38  type interactorFunc func(prompt string, choices ...interact.Choice) Resolver
    39  
    40  func (f interactorFunc) NewInteraction(prompt string, choices ...interact.Choice) Resolver {
    41  	return f(prompt, choices...)
    42  }
    43  
    44  type interactionWrapper struct {
    45  	interact.Interaction
    46  }
    47  
    48  func (w *interactionWrapper) SetIn(in io.Reader) {
    49  	w.Input = in
    50  }
    51  
    52  func (w *interactionWrapper) SetOut(o io.Writer) {
    53  	w.Output = o
    54  }
    55  
    56  //go:generate counterfeiter . Exiter
    57  
    58  // Exiter hides os.Exit for testing purposes
    59  type Exiter interface {
    60  	Exit(code int)
    61  }
    62  
    63  type exiterFunc func(int)
    64  
    65  func (f exiterFunc) Exit(code int) {
    66  	f(code)
    67  }
    68  
    69  // UI is interface to interact with the user
    70  type UI struct {
    71  	// In is the input buffer
    72  	In io.Reader
    73  	// Out is the output buffer
    74  	Out io.Writer
    75  	// OutForInteration is the output buffer when working with go-interact. When
    76  	// working with Windows, color.Output does not work with TTY detection. So
    77  	// real STDOUT is required or go-interact will not properly work.
    78  	OutForInteration io.Writer
    79  	// Err is the error buffer
    80  	Err io.Writer
    81  
    82  	colorEnabled configv3.ColorSetting
    83  	translate    TranslateFunc
    84  	Exiter       Exiter
    85  
    86  	terminalLock *sync.Mutex
    87  	fileLock     *sync.Mutex
    88  
    89  	Interactor Interactor
    90  
    91  	IsTTY         bool
    92  	TerminalWidth int
    93  
    94  	TimezoneLocation *time.Location
    95  
    96  	deferred []string
    97  }
    98  
    99  // NewUI will return a UI object where Out is set to STDOUT, In is set to
   100  // STDIN, and Err is set to STDERR
   101  func NewUI(config Config) (*UI, error) {
   102  	translateFunc, err := GetTranslationFunc(config)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	location := time.Now().Location()
   108  
   109  	return &UI{
   110  		In:               os.Stdin,
   111  		Out:              color.Output,
   112  		OutForInteration: os.Stdout,
   113  		Err:              os.Stderr,
   114  		colorEnabled:     config.ColorEnabled(),
   115  		translate:        translateFunc,
   116  		terminalLock:     &sync.Mutex{},
   117  		Exiter:           realExiter,
   118  		fileLock:         &sync.Mutex{},
   119  		Interactor:       realInteract,
   120  		IsTTY:            config.IsTTY(),
   121  		TerminalWidth:    config.TerminalWidth(),
   122  		TimezoneLocation: location,
   123  	}, nil
   124  }
   125  
   126  // NewPluginUI will return a UI object where OUT and ERR are customizable.
   127  func NewPluginUI(config Config, outBuffer io.Writer, errBuffer io.Writer) (*UI, error) {
   128  	translateFunc, translationError := GetTranslationFunc(config)
   129  	if translationError != nil {
   130  		return nil, translationError
   131  	}
   132  
   133  	location := time.Now().Location()
   134  
   135  	return &UI{
   136  		In:               nil,
   137  		Out:              outBuffer,
   138  		OutForInteration: outBuffer,
   139  		Err:              errBuffer,
   140  		colorEnabled:     configv3.ColorDisabled,
   141  		translate:        translateFunc,
   142  		terminalLock:     &sync.Mutex{},
   143  		Exiter:           realExiter,
   144  		fileLock:         &sync.Mutex{},
   145  		Interactor:       realInteract,
   146  		IsTTY:            config.IsTTY(),
   147  		TerminalWidth:    config.TerminalWidth(),
   148  		TimezoneLocation: location,
   149  	}, nil
   150  }
   151  
   152  // NewTestUI will return a UI object where Out, In, and Err are customizable,
   153  // and colors are disabled
   154  func NewTestUI(in io.Reader, out io.Writer, err io.Writer) *UI {
   155  	translationFunc, translateErr := generateTranslationFunc([]byte("[]"))
   156  	if translateErr != nil {
   157  		panic(translateErr)
   158  	}
   159  
   160  	return &UI{
   161  		In:               in,
   162  		Out:              out,
   163  		OutForInteration: out,
   164  		Err:              err,
   165  		Exiter:           realExiter,
   166  		colorEnabled:     configv3.ColorDisabled,
   167  		translate:        translationFunc,
   168  		Interactor:       realInteract,
   169  		terminalLock:     &sync.Mutex{},
   170  		fileLock:         &sync.Mutex{},
   171  		TimezoneLocation: time.UTC,
   172  	}
   173  }
   174  
   175  // DeferText translates the template, substitutes in templateValues, and
   176  // Enqueues the output to be presented later via FlushDeferred. Only the first
   177  // map in templateValues is used.
   178  func (ui *UI) DeferText(template string, templateValues ...map[string]interface{}) {
   179  	s := fmt.Sprintf("%s\n", ui.TranslateText(template, templateValues...))
   180  	ui.deferred = append(ui.deferred, s)
   181  }
   182  
   183  func (ui *UI) DisplayDeprecationWarning() {
   184  	ui.terminalLock.Lock()
   185  	defer ui.terminalLock.Unlock()
   186  
   187  	fmt.Fprintf(ui.Err, "Deprecation warning: This command has been deprecated. This feature will be removed in the future.\n")
   188  }
   189  
   190  // DisplayError outputs the translated error message to ui.Err if the error
   191  // satisfies TranslatableError, otherwise it outputs the original error message
   192  // to ui.Err. It also outputs "FAILED" in bold red to ui.Out.
   193  func (ui *UI) DisplayError(err error) {
   194  	var errMsg string
   195  	if translatableError, ok := err.(translatableerror.TranslatableError); ok {
   196  		errMsg = translatableError.Translate(ui.translate)
   197  	} else {
   198  		errMsg = err.Error()
   199  	}
   200  	fmt.Fprintf(ui.Err, "%s\n", errMsg)
   201  
   202  	ui.terminalLock.Lock()
   203  	defer ui.terminalLock.Unlock()
   204  
   205  	fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText("FAILED"), color.New(color.FgRed, color.Bold)))
   206  }
   207  
   208  func (ui *UI) DisplayFileDeprecationWarning() {
   209  	ui.terminalLock.Lock()
   210  	defer ui.terminalLock.Unlock()
   211  
   212  	fmt.Fprintf(ui.Err, "Deprecation warning: This command has been deprecated and will be removed in the future. For similar functionality, please use the `cf ssh` command instead.\n")
   213  }
   214  
   215  // DisplayHeader translates the header, bolds and adds the default color to the
   216  // header, and outputs the result to ui.Out.
   217  func (ui *UI) DisplayHeader(text string) {
   218  	ui.terminalLock.Lock()
   219  	defer ui.terminalLock.Unlock()
   220  
   221  	fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText(text), color.New(color.Bold)))
   222  }
   223  
   224  // DisplayNewline outputs a newline to UI.Out.
   225  func (ui *UI) DisplayNewline() {
   226  	ui.terminalLock.Lock()
   227  	defer ui.terminalLock.Unlock()
   228  
   229  	fmt.Fprintf(ui.Out, "\n")
   230  }
   231  
   232  // DisplayOK outputs a bold green translated "OK" to UI.Out.
   233  func (ui *UI) DisplayOK() {
   234  	ui.terminalLock.Lock()
   235  	defer ui.terminalLock.Unlock()
   236  
   237  	fmt.Fprintf(ui.Out, "%s\n\n", ui.modifyColor(ui.TranslateText("OK"), color.New(color.FgGreen, color.Bold)))
   238  }
   239  
   240  // DisplayText translates the template, substitutes in templateValues, and
   241  // outputs the result to ui.Out. Only the first map in templateValues is used.
   242  func (ui *UI) DisplayText(template string, templateValues ...map[string]interface{}) {
   243  	ui.terminalLock.Lock()
   244  	defer ui.terminalLock.Unlock()
   245  
   246  	fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, templateValues...))
   247  }
   248  
   249  // DisplayTextWithBold translates the template, bolds the templateValues,
   250  // substitutes templateValues into the template, and outputs
   251  // the result to ui.Out. Only the first map in templateValues is used.
   252  func (ui *UI) DisplayTextWithBold(template string, templateValues ...map[string]interface{}) {
   253  	ui.terminalLock.Lock()
   254  	defer ui.terminalLock.Unlock()
   255  
   256  	firstTemplateValues := getFirstSet(templateValues)
   257  	for key, value := range firstTemplateValues {
   258  		firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.Bold))
   259  	}
   260  	fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues))
   261  }
   262  
   263  // DisplayTextWithFlavor translates the template, bolds and adds cyan color to
   264  // templateValues, substitutes templateValues into the template, and outputs
   265  // the result to ui.Out. Only the first map in templateValues is used.
   266  func (ui *UI) DisplayTextWithFlavor(template string, templateValues ...map[string]interface{}) {
   267  	ui.terminalLock.Lock()
   268  	defer ui.terminalLock.Unlock()
   269  
   270  	firstTemplateValues := getFirstSet(templateValues)
   271  	for key, value := range firstTemplateValues {
   272  		firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.FgCyan, color.Bold))
   273  	}
   274  	fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues))
   275  }
   276  
   277  // FlushDeferred displays text previously deferred (using DeferText) to the UI's
   278  // `Out`.
   279  func (ui *UI) FlushDeferred() {
   280  	ui.terminalLock.Lock()
   281  	defer ui.terminalLock.Unlock()
   282  
   283  	for _, s := range ui.deferred {
   284  		fmt.Fprint(ui.Out, s)
   285  	}
   286  	ui.deferred = []string{}
   287  }
   288  
   289  // GetErr returns the error writer.
   290  func (ui *UI) GetErr() io.Writer {
   291  	return ui.Err
   292  }
   293  
   294  // GetIn returns the input reader.
   295  func (ui *UI) GetIn() io.Reader {
   296  	return ui.In
   297  }
   298  
   299  // GetOut returns the output writer. Same as `Writer`.
   300  func (ui *UI) GetOut() io.Writer {
   301  	return ui.Out
   302  }
   303  
   304  // TranslateText passes the template through an internationalization function
   305  // to translate it to a pre-configured language, and returns the template with
   306  // templateValues substituted in. Only the first map in templateValues is used.
   307  func (ui *UI) TranslateText(template string, templateValues ...map[string]interface{}) string {
   308  	return ui.translate(template, getFirstSet(templateValues))
   309  }
   310  
   311  // UserFriendlyDate converts the time to UTC and then formats it to ISO8601.
   312  func (ui *UI) UserFriendlyDate(input time.Time) string {
   313  	return input.Local().Format("Mon 02 Jan 15:04:05 MST 2006")
   314  }
   315  
   316  // Writer returns the output writer. Same as `GetOut`.
   317  func (ui *UI) Writer() io.Writer {
   318  	return ui.Out
   319  }
   320  
   321  func (ui *UI) displayWrappingTableWithWidth(prefix string, table [][]string, padding int) {
   322  	ui.terminalLock.Lock()
   323  	defer ui.terminalLock.Unlock()
   324  
   325  	var columnPadding []int
   326  
   327  	rows := len(table)
   328  	columns := len(table[0])
   329  
   330  	for col := 0; col < columns-1; col++ {
   331  		var max int
   332  		for row := 0; row < rows; row++ {
   333  			if strLen := runewidth.StringWidth(table[row][col]); max < strLen {
   334  				max = strLen
   335  			}
   336  		}
   337  		columnPadding = append(columnPadding, max+padding)
   338  	}
   339  
   340  	spilloverPadding := len(prefix) + sum(columnPadding)
   341  	lastColumnWidth := ui.TerminalWidth - spilloverPadding
   342  
   343  	for row := 0; row < rows; row++ {
   344  		fmt.Fprint(ui.Out, prefix)
   345  
   346  		// for all columns except last, add cell value and padding
   347  		for col := 0; col < columns-1; col++ {
   348  			var addedPadding int
   349  			if col+1 != columns {
   350  				addedPadding = columnPadding[col] - runewidth.StringWidth(table[row][col])
   351  			}
   352  			fmt.Fprintf(ui.Out, "%s%s", table[row][col], strings.Repeat(" ", addedPadding))
   353  		}
   354  
   355  		// 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
   356  		words := strings.Split(table[row][columns-1], " ")
   357  		currentWidth := 0
   358  
   359  		for _, word := range words {
   360  			wordWidth := runewidth.StringWidth(word)
   361  			switch {
   362  			case currentWidth == 0:
   363  				currentWidth = wordWidth
   364  				fmt.Fprintf(ui.Out, "%s", word)
   365  			case (wordWidth + 1 + currentWidth) > lastColumnWidth:
   366  				fmt.Fprintf(ui.Out, "\n%s%s", strings.Repeat(" ", spilloverPadding), word)
   367  				currentWidth = wordWidth
   368  			default:
   369  				fmt.Fprintf(ui.Out, " %s", word)
   370  				currentWidth += wordWidth + 1
   371  			}
   372  		}
   373  
   374  		fmt.Fprintf(ui.Out, "\n")
   375  	}
   376  }
   377  
   378  func (ui *UI) modifyColor(text string, colorPrinter *color.Color) string {
   379  	if len(text) == 0 {
   380  		return text
   381  	}
   382  
   383  	switch ui.colorEnabled {
   384  	case configv3.ColorEnabled:
   385  		colorPrinter.EnableColor()
   386  	case configv3.ColorDisabled:
   387  		colorPrinter.DisableColor()
   388  	}
   389  
   390  	return colorPrinter.SprintFunc()(text)
   391  }
   392  
   393  // getFirstSet returns the first map if 1 or more maps are provided. Otherwise
   394  // it returns the empty map.
   395  func getFirstSet(list []map[string]interface{}) map[string]interface{} {
   396  	if len(list) == 0 {
   397  		return map[string]interface{}{}
   398  	}
   399  	return list[0]
   400  }
   401  
   402  func sum(intSlice []int) int {
   403  	sum := 0
   404  
   405  	for _, i := range intSlice {
   406  		sum += i
   407  	}
   408  
   409  	return sum
   410  }