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