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