storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/console/console.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2019 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  // Package console implements console printing helpers
    18  package console
    19  
    20  import (
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  	"sync"
    26  
    27  	"github.com/fatih/color"
    28  	"github.com/mattn/go-colorable"
    29  	"github.com/mattn/go-isatty"
    30  )
    31  
    32  var (
    33  	// Used by the caller to print multiple lines atomically. Exposed by Lock/Unlock methods.
    34  	publicMutex sync.Mutex
    35  
    36  	// Used internally by console.
    37  	privateMutex sync.Mutex
    38  
    39  	stderrColoredOutput = colorable.NewColorableStderr()
    40  
    41  	// Print prints a message.
    42  	Print = func(data ...interface{}) {
    43  		consolePrint("Print", Theme["Print"], data...)
    44  	}
    45  
    46  	// PrintC prints a message with color.
    47  	PrintC = func(data ...interface{}) {
    48  		consolePrint("PrintC", Theme["PrintC"], data...)
    49  	}
    50  
    51  	// Printf prints a formatted message.
    52  	Printf = func(format string, data ...interface{}) {
    53  		consolePrintf("Print", Theme["Print"], format, data...)
    54  	}
    55  
    56  	// Println prints a message with a newline.
    57  	Println = func(data ...interface{}) {
    58  		consolePrintln("Print", Theme["Print"], data...)
    59  	}
    60  
    61  	// Fatal print a error message and exit.
    62  	Fatal = func(data ...interface{}) {
    63  		consolePrint("Fatal", Theme["Fatal"], data...)
    64  		os.Exit(1)
    65  	}
    66  
    67  	// Fatalf print a error message with a format specified and exit.
    68  	Fatalf = func(format string, data ...interface{}) {
    69  		consolePrintf("Fatal", Theme["Fatal"], format, data...)
    70  		os.Exit(1)
    71  	}
    72  
    73  	// Fatalln print a error message with a new line and exit.
    74  	Fatalln = func(data ...interface{}) {
    75  		consolePrintln("Fatal", Theme["Fatal"], data...)
    76  		os.Exit(1)
    77  	}
    78  
    79  	// Error prints a error message.
    80  	Error = func(data ...interface{}) {
    81  		consolePrint("Error", Theme["Error"], data...)
    82  	}
    83  
    84  	// Errorf print a error message with a format specified.
    85  	Errorf = func(format string, data ...interface{}) {
    86  		consolePrintf("Error", Theme["Error"], format, data...)
    87  	}
    88  
    89  	// Errorln prints a error message with a new line.
    90  	Errorln = func(data ...interface{}) {
    91  		consolePrintln("Error", Theme["Error"], data...)
    92  	}
    93  
    94  	// Info prints a informational message.
    95  	Info = func(data ...interface{}) {
    96  		consolePrint("Info", Theme["Info"], data...)
    97  	}
    98  
    99  	// Infof prints a informational message in custom format.
   100  	Infof = func(format string, data ...interface{}) {
   101  		consolePrintf("Info", Theme["Info"], format, data...)
   102  	}
   103  
   104  	// Infoln prints a informational message with a new line.
   105  	Infoln = func(data ...interface{}) {
   106  		consolePrintln("Info", Theme["Info"], data...)
   107  	}
   108  
   109  	// Debug prints a debug message without a new line
   110  	// Debug prints a debug message.
   111  	Debug = func(data ...interface{}) {
   112  		consolePrint("Debug", Theme["Debug"], data...)
   113  	}
   114  
   115  	// Debugf prints a debug message with a new line.
   116  	Debugf = func(format string, data ...interface{}) {
   117  		consolePrintf("Debug", Theme["Debug"], format, data...)
   118  	}
   119  
   120  	// Debugln prints a debug message with a new line.
   121  	Debugln = func(data ...interface{}) {
   122  		consolePrintln("Debug", Theme["Debug"], data...)
   123  	}
   124  
   125  	// Colorize prints message in a colorized form, dictated by the corresponding tag argument.
   126  	Colorize = func(tag string, data interface{}) string {
   127  		if isatty.IsTerminal(os.Stdout.Fd()) {
   128  			colorized, ok := Theme[tag]
   129  			if ok {
   130  				return colorized.SprintFunc()(data)
   131  			} // else: No theme found. Return as string.
   132  		}
   133  		return fmt.Sprint(data)
   134  	}
   135  
   136  	// Eraseline Print in new line and adjust to top so that we don't print over the ongoing progress bar.
   137  	Eraseline = func() {
   138  		consolePrintf("Print", Theme["Print"], "%c[2K\n", 27)
   139  		consolePrintf("Print", Theme["Print"], "%c[A", 27)
   140  	}
   141  )
   142  
   143  // wrap around standard fmt functions.
   144  // consolePrint prints a message prefixed with message type and program name.
   145  func consolePrint(tag string, c *color.Color, a ...interface{}) {
   146  	privateMutex.Lock()
   147  	defer privateMutex.Unlock()
   148  
   149  	switch tag {
   150  	case "Debug":
   151  		// if no arguments are given do not invoke debug printer.
   152  		if len(a) == 0 {
   153  			return
   154  		}
   155  		output := color.Output
   156  		color.Output = stderrColoredOutput
   157  		if isatty.IsTerminal(os.Stderr.Fd()) {
   158  			c.Print(ProgramName() + ": <DEBUG> ")
   159  			c.Print(a...)
   160  		} else {
   161  			fmt.Fprint(color.Output, ProgramName()+": <DEBUG> ")
   162  			fmt.Fprint(color.Output, a...)
   163  		}
   164  		color.Output = output
   165  	case "Fatal":
   166  		fallthrough
   167  	case "Error":
   168  		// if no arguments are given do not invoke fatal and error printer.
   169  		if len(a) == 0 {
   170  			return
   171  		}
   172  		output := color.Output
   173  		color.Output = stderrColoredOutput
   174  		if isatty.IsTerminal(os.Stderr.Fd()) {
   175  			c.Print(ProgramName() + ": <ERROR> ")
   176  			c.Print(a...)
   177  		} else {
   178  			fmt.Fprint(color.Output, ProgramName()+": <ERROR> ")
   179  			fmt.Fprint(color.Output, a...)
   180  		}
   181  		color.Output = output
   182  	case "Info":
   183  		// if no arguments are given do not invoke info printer.
   184  		if len(a) == 0 {
   185  			return
   186  		}
   187  		if isatty.IsTerminal(os.Stdout.Fd()) {
   188  			c.Print(ProgramName() + ": ")
   189  			c.Print(a...)
   190  		} else {
   191  			fmt.Fprint(color.Output, ProgramName()+": ")
   192  			fmt.Fprint(color.Output, a...)
   193  		}
   194  	default:
   195  		if isatty.IsTerminal(os.Stdout.Fd()) {
   196  			c.Print(a...)
   197  		} else {
   198  			fmt.Fprint(color.Output, a...)
   199  		}
   200  	}
   201  }
   202  
   203  // consolePrintf - same as print with a new line.
   204  func consolePrintf(tag string, c *color.Color, format string, a ...interface{}) {
   205  	privateMutex.Lock()
   206  	defer privateMutex.Unlock()
   207  
   208  	switch tag {
   209  	case "Debug":
   210  		// if no arguments are given do not invoke debug printer.
   211  		if len(a) == 0 {
   212  			return
   213  		}
   214  		output := color.Output
   215  		color.Output = stderrColoredOutput
   216  		if isatty.IsTerminal(os.Stderr.Fd()) {
   217  			c.Print(ProgramName() + ": <DEBUG> ")
   218  			c.Printf(format, a...)
   219  		} else {
   220  			fmt.Fprint(color.Output, ProgramName()+": <DEBUG> ")
   221  			fmt.Fprintf(color.Output, format, a...)
   222  		}
   223  		color.Output = output
   224  	case "Fatal":
   225  		fallthrough
   226  	case "Error":
   227  		// if no arguments are given do not invoke fatal and error printer.
   228  		if len(a) == 0 {
   229  			return
   230  		}
   231  		output := color.Output
   232  		color.Output = stderrColoredOutput
   233  		if isatty.IsTerminal(os.Stderr.Fd()) {
   234  			c.Print(ProgramName() + ": <ERROR> ")
   235  			c.Printf(format, a...)
   236  		} else {
   237  			fmt.Fprint(color.Output, ProgramName()+": <ERROR> ")
   238  			fmt.Fprintf(color.Output, format, a...)
   239  		}
   240  		color.Output = output
   241  	case "Info":
   242  		// if no arguments are given do not invoke info printer.
   243  		if len(a) == 0 {
   244  			return
   245  		}
   246  		if isatty.IsTerminal(os.Stdout.Fd()) {
   247  			c.Print(ProgramName() + ": ")
   248  			c.Printf(format, a...)
   249  		} else {
   250  			fmt.Fprint(color.Output, ProgramName()+": ")
   251  			fmt.Fprintf(color.Output, format, a...)
   252  		}
   253  	default:
   254  		if isatty.IsTerminal(os.Stdout.Fd()) {
   255  			c.Printf(format, a...)
   256  		} else {
   257  			fmt.Fprintf(color.Output, format, a...)
   258  		}
   259  	}
   260  }
   261  
   262  // consolePrintln - same as print with a new line.
   263  func consolePrintln(tag string, c *color.Color, a ...interface{}) {
   264  	privateMutex.Lock()
   265  	defer privateMutex.Unlock()
   266  
   267  	switch tag {
   268  	case "Debug":
   269  		// if no arguments are given do not invoke debug printer.
   270  		if len(a) == 0 {
   271  			return
   272  		}
   273  		output := color.Output
   274  		color.Output = stderrColoredOutput
   275  		if isatty.IsTerminal(os.Stderr.Fd()) {
   276  			c.Print(ProgramName() + ": <DEBUG> ")
   277  			c.Println(a...)
   278  		} else {
   279  			fmt.Fprint(color.Output, ProgramName()+": <DEBUG> ")
   280  			fmt.Fprintln(color.Output, a...)
   281  		}
   282  		color.Output = output
   283  	case "Fatal":
   284  		fallthrough
   285  	case "Error":
   286  		// if no arguments are given do not invoke fatal and error printer.
   287  		if len(a) == 0 {
   288  			return
   289  		}
   290  		output := color.Output
   291  		color.Output = stderrColoredOutput
   292  		if isatty.IsTerminal(os.Stderr.Fd()) {
   293  			c.Print(ProgramName() + ": <ERROR> ")
   294  			c.Println(a...)
   295  		} else {
   296  			fmt.Fprint(color.Output, ProgramName()+": <ERROR> ")
   297  			fmt.Fprintln(color.Output, a...)
   298  		}
   299  		color.Output = output
   300  	case "Info":
   301  		// if no arguments are given do not invoke info printer.
   302  		if len(a) == 0 {
   303  			return
   304  		}
   305  		if isatty.IsTerminal(os.Stdout.Fd()) {
   306  			c.Print(ProgramName() + ": ")
   307  			c.Println(a...)
   308  		} else {
   309  			fmt.Fprint(color.Output, ProgramName()+": ")
   310  			fmt.Fprintln(color.Output, a...)
   311  		}
   312  	default:
   313  		if isatty.IsTerminal(os.Stdout.Fd()) {
   314  			c.Println(a...)
   315  		} else {
   316  			fmt.Fprintln(color.Output, a...)
   317  		}
   318  	}
   319  }
   320  
   321  // Lock console.
   322  func Lock() {
   323  	publicMutex.Lock()
   324  }
   325  
   326  // Unlock locked console.
   327  func Unlock() {
   328  	publicMutex.Unlock()
   329  }
   330  
   331  // ProgramName - return the name of the executable program.
   332  func ProgramName() string {
   333  	_, progName := filepath.Split(os.Args[0])
   334  	return progName
   335  }
   336  
   337  // Table - data to print in table format with fixed row widths.
   338  type Table struct {
   339  	// per-row colors
   340  	RowColors []*color.Color
   341  
   342  	// per-column align-right flag (aligns left by default)
   343  	AlignRight []bool
   344  
   345  	// Left margin width for table
   346  	TableIndentWidth int
   347  
   348  	// Flag to print separator under heading. Row 0 is considered heading
   349  	HeaderRowSeparator bool
   350  }
   351  
   352  // NewTable - create a new Table instance. Takes per-row colors and
   353  // per-column right-align flags and table indentation width (i.e. left
   354  // margin width)
   355  func NewTable(rowColors []*color.Color, alignRight []bool, indentWidth int) *Table {
   356  	return &Table{rowColors, alignRight, indentWidth, false}
   357  }
   358  
   359  // DisplayTable - prints the table
   360  func (t *Table) DisplayTable(rows [][]string) error {
   361  	numRows := len(rows)
   362  	numCols := len(rows[0])
   363  	if numRows != len(t.RowColors) {
   364  		return fmt.Errorf("row count and row-colors mismatch")
   365  	}
   366  
   367  	// Compute max. column widths
   368  	maxColWidths := make([]int, numCols)
   369  	for _, row := range rows {
   370  		if len(row) != len(t.AlignRight) {
   371  			return fmt.Errorf("col count and align-right mismatch")
   372  		}
   373  		for i, v := range row {
   374  			if len([]rune(v)) > maxColWidths[i] {
   375  				maxColWidths[i] = len([]rune(v))
   376  			}
   377  		}
   378  	}
   379  
   380  	// Compute per-cell text with padding and alignment applied.
   381  	paddedText := make([][]string, numRows)
   382  	for r, row := range rows {
   383  		paddedText[r] = make([]string, numCols)
   384  		for c, cell := range row {
   385  			if t.AlignRight[c] {
   386  				fmtStr := fmt.Sprintf("%%%ds", maxColWidths[c])
   387  				paddedText[r][c] = fmt.Sprintf(fmtStr, cell)
   388  			} else {
   389  				extraWidth := maxColWidths[c] - len([]rune(cell))
   390  				fmtStr := fmt.Sprintf("%%s%%%ds", extraWidth)
   391  				paddedText[r][c] = fmt.Sprintf(fmtStr, cell, "")
   392  			}
   393  		}
   394  	}
   395  
   396  	// Draw table top border
   397  	segments := make([]string, numCols)
   398  	for i, c := range maxColWidths {
   399  		segments[i] = strings.Repeat("─", c+2)
   400  	}
   401  	indentText := strings.Repeat(" ", t.TableIndentWidth)
   402  	border := fmt.Sprintf("%s┌%s┐", indentText, strings.Join(segments, "┬"))
   403  	fmt.Println(border)
   404  
   405  	// Print the table with colors
   406  	for r, row := range paddedText {
   407  		if t.HeaderRowSeparator && r == 1 {
   408  			// Draw table header-row border
   409  			border = fmt.Sprintf("%s├%s┤", indentText, strings.Join(segments, "┼"))
   410  			fmt.Println(border)
   411  		}
   412  		fmt.Print(indentText + "│ ")
   413  		for c, text := range row {
   414  			t.RowColors[r].Print(text)
   415  			if c != numCols-1 {
   416  				fmt.Print(" │ ")
   417  			}
   418  		}
   419  		fmt.Println(" │")
   420  	}
   421  
   422  	// Draw table bottom border
   423  	border = fmt.Sprintf("%s└%s┘", indentText, strings.Join(segments, "┴"))
   424  	fmt.Println(border)
   425  
   426  	return nil
   427  }
   428  
   429  // RewindLines - uses terminal escape symbols to clear and rewind
   430  // upwards on the console for `n` lines.
   431  func RewindLines(n int) {
   432  	for i := 0; i < n; i++ {
   433  		fmt.Printf("\033[1A\033[K")
   434  	}
   435  }