github.com/arunkumar7540/cli@v6.45.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  
    97  // NewUI will return a UI object where Out is set to STDOUT, In is set to
    98  // STDIN, and Err is set to STDERR
    99  func NewUI(config Config) (*UI, error) {
   100  	translateFunc, err := GetTranslationFunc(config)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	location := time.Now().Location()
   106  
   107  	return &UI{
   108  		In:               os.Stdin,
   109  		Out:              color.Output,
   110  		OutForInteration: os.Stdout,
   111  		Err:              os.Stderr,
   112  		colorEnabled:     config.ColorEnabled(),
   113  		translate:        translateFunc,
   114  		terminalLock:     &sync.Mutex{},
   115  		Exiter:           realExiter,
   116  		fileLock:         &sync.Mutex{},
   117  		Interactor:       realInteract,
   118  		IsTTY:            config.IsTTY(),
   119  		TerminalWidth:    config.TerminalWidth(),
   120  		TimezoneLocation: location,
   121  	}, nil
   122  }
   123  
   124  // NewTestUI will return a UI object where Out, In, and Err are customizable,
   125  // and colors are disabled
   126  func NewTestUI(in io.Reader, out io.Writer, err io.Writer) *UI {
   127  	translationFunc, translateErr := generateTranslationFunc([]byte("[]"))
   128  	if translateErr != nil {
   129  		panic(translateErr)
   130  	}
   131  
   132  	return &UI{
   133  		In:               in,
   134  		Out:              out,
   135  		OutForInteration: out,
   136  		Err:              err,
   137  		Exiter:           realExiter,
   138  		colorEnabled:     configv3.ColorDisabled,
   139  		translate:        translationFunc,
   140  		Interactor:       realInteract,
   141  		terminalLock:     &sync.Mutex{},
   142  		fileLock:         &sync.Mutex{},
   143  		TimezoneLocation: time.UTC,
   144  	}
   145  }
   146  
   147  func (ui *UI) DisplayDeprecationWarning() {
   148  	ui.terminalLock.Lock()
   149  	defer ui.terminalLock.Unlock()
   150  
   151  	fmt.Fprintf(ui.Err, "Deprecation warning: This command has been deprecated. This feature will be removed in the future.\n")
   152  }
   153  
   154  // DisplayError outputs the translated error message to ui.Err if the error
   155  // satisfies TranslatableError, otherwise it outputs the original error message
   156  // to ui.Err. It also outputs "FAILED" in bold red to ui.Out.
   157  func (ui *UI) DisplayError(err error) {
   158  	var errMsg string
   159  	if translatableError, ok := err.(translatableerror.TranslatableError); ok {
   160  		errMsg = translatableError.Translate(ui.translate)
   161  	} else {
   162  		errMsg = err.Error()
   163  	}
   164  	fmt.Fprintf(ui.Err, "%s\n", errMsg)
   165  
   166  	ui.terminalLock.Lock()
   167  	defer ui.terminalLock.Unlock()
   168  
   169  	fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText("FAILED"), color.New(color.FgRed, color.Bold)))
   170  }
   171  
   172  func (ui *UI) DisplayFileDeprecationWarning() {
   173  	ui.terminalLock.Lock()
   174  	defer ui.terminalLock.Unlock()
   175  
   176  	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")
   177  }
   178  
   179  // DisplayHeader translates the header, bolds and adds the default color to the
   180  // header, and outputs the result to ui.Out.
   181  func (ui *UI) DisplayHeader(text string) {
   182  	ui.terminalLock.Lock()
   183  	defer ui.terminalLock.Unlock()
   184  
   185  	fmt.Fprintf(ui.Out, "%s\n", ui.modifyColor(ui.TranslateText(text), color.New(color.Bold)))
   186  }
   187  
   188  // DisplayNewline outputs a newline to UI.Out.
   189  func (ui *UI) DisplayNewline() {
   190  	ui.terminalLock.Lock()
   191  	defer ui.terminalLock.Unlock()
   192  
   193  	fmt.Fprintf(ui.Out, "\n")
   194  }
   195  
   196  // DisplayOK outputs a bold green translated "OK" to UI.Out.
   197  func (ui *UI) DisplayOK() {
   198  	ui.terminalLock.Lock()
   199  	defer ui.terminalLock.Unlock()
   200  
   201  	fmt.Fprintf(ui.Out, "%s\n\n", ui.modifyColor(ui.TranslateText("OK"), color.New(color.FgGreen, color.Bold)))
   202  }
   203  
   204  // DisplayText translates the template, substitutes in templateValues, and
   205  // outputs the result to ui.Out. Only the first map in templateValues is used.
   206  func (ui *UI) DisplayText(template string, templateValues ...map[string]interface{}) {
   207  	ui.terminalLock.Lock()
   208  	defer ui.terminalLock.Unlock()
   209  
   210  	fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, templateValues...))
   211  }
   212  
   213  // DisplayTextWithBold translates the template, bolds the templateValues,
   214  // substitutes templateValues into the template, and outputs
   215  // the result to ui.Out. Only the first map in templateValues is used.
   216  func (ui *UI) DisplayTextWithBold(template string, templateValues ...map[string]interface{}) {
   217  	ui.terminalLock.Lock()
   218  	defer ui.terminalLock.Unlock()
   219  
   220  	firstTemplateValues := getFirstSet(templateValues)
   221  	for key, value := range firstTemplateValues {
   222  		firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.Bold))
   223  	}
   224  	fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues))
   225  }
   226  
   227  // DisplayTextWithFlavor translates the template, bolds and adds cyan color to
   228  // templateValues, substitutes templateValues into the template, and outputs
   229  // the result to ui.Out. Only the first map in templateValues is used.
   230  func (ui *UI) DisplayTextWithFlavor(template string, templateValues ...map[string]interface{}) {
   231  	ui.terminalLock.Lock()
   232  	defer ui.terminalLock.Unlock()
   233  
   234  	firstTemplateValues := getFirstSet(templateValues)
   235  	for key, value := range firstTemplateValues {
   236  		firstTemplateValues[key] = ui.modifyColor(fmt.Sprint(value), color.New(color.FgCyan, color.Bold))
   237  	}
   238  	fmt.Fprintf(ui.Out, "%s\n", ui.TranslateText(template, firstTemplateValues))
   239  }
   240  
   241  // DisplayWarning translates the warning, substitutes in templateValues, and
   242  // outputs to ui.Err. Only the first map in templateValues is used.
   243  func (ui *UI) DisplayWarning(template string, templateValues ...map[string]interface{}) {
   244  	fmt.Fprintf(ui.Err, "%s\n\n", ui.TranslateText(template, templateValues...))
   245  }
   246  
   247  // DisplayWarnings translates the warnings and outputs to ui.Err.
   248  func (ui *UI) DisplayWarnings(warnings []string) {
   249  	for _, warning := range warnings {
   250  		fmt.Fprintf(ui.Err, "%s\n", ui.TranslateText(warning))
   251  	}
   252  	if len(warnings) > 0 {
   253  		fmt.Fprintln(ui.Err)
   254  	}
   255  }
   256  
   257  // GetErr returns the error writer.
   258  func (ui *UI) GetErr() io.Writer {
   259  	return ui.Err
   260  }
   261  
   262  // GetIn returns the input reader.
   263  func (ui *UI) GetIn() io.Reader {
   264  	return ui.In
   265  }
   266  
   267  // GetOut returns the output writer. Same as `Writer`.
   268  func (ui *UI) GetOut() io.Writer {
   269  	return ui.Out
   270  }
   271  
   272  // TranslateText passes the template through an internationalization function
   273  // to translate it to a pre-configured language, and returns the template with
   274  // templateValues substituted in. Only the first map in templateValues is used.
   275  func (ui *UI) TranslateText(template string, templateValues ...map[string]interface{}) string {
   276  	return ui.translate(template, getFirstSet(templateValues))
   277  }
   278  
   279  // UserFriendlyDate converts the time to UTC and then formats it to ISO8601.
   280  func (ui *UI) UserFriendlyDate(input time.Time) string {
   281  	return input.Local().Format("Mon 02 Jan 15:04:05 MST 2006")
   282  }
   283  
   284  // Writer returns the output writer. Same as `GetOut`.
   285  func (ui *UI) Writer() io.Writer {
   286  	return ui.Out
   287  }
   288  
   289  func (ui *UI) displayWrappingTableWithWidth(prefix string, table [][]string, padding int) {
   290  	ui.terminalLock.Lock()
   291  	defer ui.terminalLock.Unlock()
   292  
   293  	var columnPadding []int
   294  
   295  	rows := len(table)
   296  	columns := len(table[0])
   297  
   298  	for col := 0; col < columns-1; col++ {
   299  		var max int
   300  		for row := 0; row < rows; row++ {
   301  			if strLen := runewidth.StringWidth(table[row][col]); max < strLen {
   302  				max = strLen
   303  			}
   304  		}
   305  		columnPadding = append(columnPadding, max+padding)
   306  	}
   307  
   308  	spilloverPadding := len(prefix) + sum(columnPadding)
   309  	lastColumnWidth := ui.TerminalWidth - spilloverPadding
   310  
   311  	for row := 0; row < rows; row++ {
   312  		fmt.Fprint(ui.Out, prefix)
   313  
   314  		// for all columns except last, add cell value and padding
   315  		for col := 0; col < columns-1; col++ {
   316  			var addedPadding int
   317  			if col+1 != columns {
   318  				addedPadding = columnPadding[col] - runewidth.StringWidth(table[row][col])
   319  			}
   320  			fmt.Fprintf(ui.Out, "%s%s", table[row][col], strings.Repeat(" ", addedPadding))
   321  		}
   322  
   323  		// 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
   324  		words := strings.Split(table[row][columns-1], " ")
   325  		currentWidth := 0
   326  
   327  		for _, word := range words {
   328  			wordWidth := runewidth.StringWidth(word)
   329  			switch {
   330  			case currentWidth == 0:
   331  				currentWidth = wordWidth
   332  				fmt.Fprintf(ui.Out, "%s", word)
   333  			case (wordWidth + 1 + currentWidth) > lastColumnWidth:
   334  				fmt.Fprintf(ui.Out, "\n%s%s", strings.Repeat(" ", spilloverPadding), word)
   335  				currentWidth = wordWidth
   336  			default:
   337  				fmt.Fprintf(ui.Out, " %s", word)
   338  				currentWidth += wordWidth + 1
   339  			}
   340  		}
   341  
   342  		fmt.Fprintf(ui.Out, "\n")
   343  	}
   344  }
   345  
   346  func (ui *UI) modifyColor(text string, colorPrinter *color.Color) string {
   347  	if len(text) == 0 {
   348  		return text
   349  	}
   350  
   351  	switch ui.colorEnabled {
   352  	case configv3.ColorEnabled:
   353  		colorPrinter.EnableColor()
   354  	case configv3.ColorDisabled:
   355  		colorPrinter.DisableColor()
   356  	}
   357  
   358  	return colorPrinter.SprintFunc()(text)
   359  }
   360  
   361  // getFirstSet returns the first map if 1 or more maps are provided. Otherwise
   362  // it returns the empty map.
   363  func getFirstSet(list []map[string]interface{}) map[string]interface{} {
   364  	if len(list) == 0 {
   365  		return map[string]interface{}{}
   366  	}
   367  	return list[0]
   368  }
   369  
   370  func sum(intSlice []int) int {
   371  	sum := 0
   372  
   373  	for _, i := range intSlice {
   374  		sum += i
   375  	}
   376  
   377  	return sum
   378  }