github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/cmd/print.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"math"
    10  	"os"
    11  	"os/exec"
    12  	"runtime"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/fatih/color"
    17  	"github.com/olekukonko/tablewriter"
    18  	"github.com/qri-io/deepdiff"
    19  	"github.com/qri-io/qri/dsref"
    20  	qrierr "github.com/qri-io/qri/errors"
    21  	"github.com/qri-io/qri/event"
    22  	"github.com/qri-io/qri/lib"
    23  	"github.com/vbauerster/mpb/v5"
    24  	"github.com/vbauerster/mpb/v5/decor"
    25  )
    26  
    27  var noPrompt = false
    28  
    29  func setNoColor(noColor bool) {
    30  	color.NoColor = noColor
    31  }
    32  
    33  func setNoPrompt(np bool) {
    34  	noPrompt = np
    35  }
    36  
    37  func printSuccess(w io.Writer, msg string, params ...interface{}) {
    38  	fmt.Fprintln(w, color.New(color.FgGreen).Sprintf(msg, params...))
    39  }
    40  
    41  func printInfo(w io.Writer, msg string, params ...interface{}) {
    42  	fmt.Fprintln(w, fmt.Sprintf(msg, params...))
    43  }
    44  
    45  func printInfoNoEndline(w io.Writer, msg string, params ...interface{}) {
    46  	fmt.Fprintf(w, fmt.Sprintf(msg, params...))
    47  }
    48  
    49  func printWarning(w io.Writer, msg string, params ...interface{}) {
    50  	fmt.Fprintln(w, color.New(color.FgYellow).Sprintf(msg, params...))
    51  }
    52  
    53  func printErr(w io.Writer, err error, params ...interface{}) {
    54  	var qerr qrierr.Error
    55  	if errors.As(err, &qerr) {
    56  		// printErr(w, fmt.Errorf(qerr.Message()))
    57  		fmt.Fprintln(w, color.New(color.FgRed).Sprintf(qerr.Message()))
    58  		return
    59  	}
    60  	fmt.Fprintln(w, color.New(color.FgRed).Sprintf(err.Error(), params...))
    61  	// if e, ok := err.(lib.Error); ok && e.Message() != "" {
    62  	// 	fmt.Fprintln(w, color.New(color.FgRed).Sprintf(e.Message(), params...))
    63  	// 	return
    64  	// }
    65  }
    66  
    67  // print a slice of stringer items to io.Writer as an indented & numbered list
    68  // offset specifies the number of items that have been skipped, index is 1-based
    69  func printItems(w io.Writer, items []fmt.Stringer, offset int) (err error) {
    70  	buf := &bytes.Buffer{}
    71  	prefix := []byte("    ")
    72  	for i, item := range items {
    73  		buf.WriteString(fmtItem(i+1+offset, item.String(), prefix))
    74  	}
    75  	return printToPager(w, buf)
    76  }
    77  
    78  // print a slice of stringer items to io.Writer as an indented & numbered list
    79  // offset specifies the number of items that have been skipped, index is 1-based
    80  func printlnStringItems(w io.Writer, items []string) (err error) {
    81  	buf := &bytes.Buffer{}
    82  	for _, item := range items {
    83  		buf.WriteString(item + "\n")
    84  	}
    85  	return printToPager(w, buf)
    86  }
    87  
    88  func printToPager(w io.Writer, buf *bytes.Buffer) (err error) {
    89  	if !stdoutIsTerminal() || noPrompt {
    90  		fmt.Fprint(w, buf.String())
    91  		return
    92  	}
    93  	// TODO (ramfox): This is POSIX specific, need to expand!
    94  	envPager := os.Getenv("PAGER")
    95  	if ok := doesCommandExist(envPager); !ok {
    96  		// if PAGER does not exist, check to see if 'more' is available on this machine
    97  		envPager = "more"
    98  		if ok := doesCommandExist(envPager); !ok {
    99  			// if 'more' does not exist, check to see if 'less' is available on this machine
   100  			envPager = "less"
   101  			if ok := doesCommandExist(envPager); !ok {
   102  				// sensible default: if none of these commands exist
   103  				// just print the results to the given io.Writer
   104  				fmt.Fprintln(w, buf.String())
   105  				return nil
   106  			}
   107  		}
   108  	}
   109  	pager := &exec.Cmd{}
   110  	os := runtime.GOOS
   111  	if os == "linux" {
   112  		pager = exec.Command("/bin/sh", "-c", envPager, "-R")
   113  	} else {
   114  		pager = exec.Command("/bin/sh", "-c", envPager+" -R")
   115  	}
   116  
   117  	pager.Stdin = buf
   118  	pager.Stdout = w
   119  	err = pager.Run()
   120  	if err != nil {
   121  		// sensible default: if something goes wrong printing to the
   122  		// pager, just print the results to the given io.Writer
   123  		fmt.Fprintln(w, buf.String())
   124  	}
   125  	return
   126  }
   127  
   128  func fmtItem(i int, item string, prefix []byte) string {
   129  	var res []byte
   130  	bol := true
   131  	b := []byte(item)
   132  	d := []byte(fmt.Sprintf("%d", i))
   133  	prefix1 := append(d, prefix[len(d):]...)
   134  	for i, c := range b {
   135  		if bol && c != '\n' {
   136  			if i == 0 {
   137  				res = append(res, prefix1...)
   138  			} else {
   139  				res = append(res, prefix...)
   140  			}
   141  		}
   142  		res = append(res, c)
   143  		bol = c == '\n'
   144  	}
   145  	return string(res)
   146  }
   147  
   148  func prompt(w io.Writer, r io.Reader, msg string) string {
   149  	var input string
   150  	printInfoNoEndline(w, msg)
   151  	fmt.Fscanln(r, &input)
   152  	return strings.TrimSpace(input)
   153  }
   154  
   155  func inputText(w io.Writer, r io.Reader, message, defaultText string) string {
   156  	input := prompt(w, r, fmt.Sprintf("%s [%s]: ", message, defaultText))
   157  	if input == "" {
   158  		input = defaultText
   159  	}
   160  
   161  	return input
   162  }
   163  
   164  func confirm(w io.Writer, r io.Reader, message string, def bool) bool {
   165  	if noPrompt {
   166  		return def
   167  	}
   168  
   169  	yellow := color.New(color.FgYellow).SprintFunc()
   170  	defaultText := "y/N"
   171  	if def {
   172  		defaultText = "Y/n"
   173  	}
   174  	input := prompt(w, r, fmt.Sprintf("%s [%s]: ", yellow(message), defaultText))
   175  	if input == "" {
   176  		return def
   177  	}
   178  	input = strings.TrimSpace(strings.ToLower(input))
   179  	return (input == "y" || input == "yes")
   180  }
   181  
   182  func usingRPCError(cmdName string) error {
   183  	return fmt.Errorf(`sorry, we can't run the '%s' command while 'qri connect' is running
   184  we know this is super irritating, and it'll be fixed in the future. 
   185  In the meantime please close qri and re-run this command`, cmdName)
   186  }
   187  
   188  func doesCommandExist(cmdName string) bool {
   189  	if cmdName == "" {
   190  		return false
   191  	}
   192  	cmd := exec.Command("/bin/sh", "-c", "command -v "+cmdName)
   193  	if err := cmd.Run(); err != nil {
   194  		return false
   195  	}
   196  	return true
   197  }
   198  
   199  func printDiff(w io.Writer, res *lib.DiffResponse, summaryOnly bool) (err error) {
   200  	buf := &bytes.Buffer{}
   201  	// TODO (b5): this reading from a package variable is pretty hacky :/
   202  	// should use the IsATTY package from mattn
   203  	deepdiff.FormatPrettyStats(buf, res.Stat, !color.NoColor)
   204  	if !summaryOnly {
   205  		buf.WriteByte('\n')
   206  		if err = deepdiff.FormatPretty(buf, res.Diff, !color.NoColor); err != nil {
   207  			return err
   208  		}
   209  	}
   210  
   211  	printToPager(w, buf)
   212  	return nil
   213  }
   214  
   215  func renderTable(writer io.Writer, header []string, data [][]string) {
   216  	table := tablewriter.NewWriter(writer)
   217  	table.SetHeader(header)
   218  	table.SetAutoWrapText(false)
   219  	table.SetAutoFormatHeaders(true)
   220  	table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
   221  	table.SetAlignment(tablewriter.ALIGN_LEFT)
   222  	table.SetCenterSeparator("")
   223  	table.SetColumnSeparator("")
   224  	table.SetRowSeparator("")
   225  	table.SetHeaderLine(false)
   226  	table.SetBorder(false)
   227  	table.SetTablePadding("  ")
   228  	table.SetNoWhiteSpace(true)
   229  	table.AppendBulk(data)
   230  	table.Render()
   231  }
   232  
   233  // PrintProgressBarsOnEvents writes save progress data to the given writer
   234  func PrintProgressBarsOnEvents(w io.Writer, bus event.Bus) {
   235  	var lock sync.Mutex
   236  	// initialize progress container, with custom width
   237  	p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(w))
   238  	progress := map[string]*mpb.Bar{}
   239  
   240  	if bus == nil {
   241  		log.Debugf("event bus is nil")
   242  		return
   243  	}
   244  	// wire up a subscription to print download progress to streams
   245  	bus.SubscribeTypes(func(_ context.Context, e event.Event) error {
   246  		lock.Lock()
   247  		defer lock.Unlock()
   248  		log.Debugw("handle event", "type", e.Type, "payload", e.Payload)
   249  
   250  		switch evt := e.Payload.(type) {
   251  		case event.DsSaveEvent:
   252  			evtID := fmt.Sprintf("%s/%s", evt.Username, evt.Name)
   253  			cpl := int64(math.Ceil(evt.Completion * 100))
   254  
   255  			switch e.Type {
   256  			case event.ETDatasetSaveStarted:
   257  				bar, exists := progress[evtID]
   258  				if !exists {
   259  					bar = addElapsedBar(p, 100, "saving")
   260  					progress[evtID] = bar
   261  				}
   262  				bar.SetCurrent(cpl)
   263  			case event.ETDatasetSaveProgress:
   264  				bar, exists := progress[evtID]
   265  				if !exists {
   266  					bar = addElapsedBar(p, 100, "saving")
   267  					progress[evtID] = bar
   268  				}
   269  				bar.SetCurrent(cpl)
   270  			case event.ETDatasetSaveCompleted:
   271  				if bar, exists := progress[evtID]; exists {
   272  					bar.SetTotal(100, true)
   273  					delete(progress, evtID)
   274  				}
   275  			}
   276  		case event.RemoteEvent:
   277  			switch e.Type {
   278  			case event.ETRemoteClientPushVersionProgress:
   279  				bar, exists := progress[evt.Ref.String()]
   280  				if !exists {
   281  					bar = addBar(p, int64(len(evt.Progress)), "pushing")
   282  					progress[evt.Ref.String()] = bar
   283  				}
   284  				bar.SetCurrent(int64(evt.Progress.CompletedBlocks()))
   285  			case event.ETRemoteClientPushVersionCompleted:
   286  				if bar, exists := progress[evt.Ref.String()]; exists {
   287  					bar.SetTotal(int64(len(evt.Progress)), true)
   288  					delete(progress, evt.Ref.String())
   289  				}
   290  
   291  			case event.ETRemoteClientPullVersionProgress:
   292  				bar, exists := progress[evt.Ref.String()]
   293  				if !exists {
   294  					bar = addBar(p, int64(len(evt.Progress)), "pulling")
   295  					progress[evt.Ref.String()] = bar
   296  				}
   297  				bar.SetCurrent(int64(evt.Progress.CompletedBlocks()))
   298  			case event.ETRemoteClientPullVersionCompleted:
   299  				if bar, exists := progress[evt.Ref.String()]; exists {
   300  					bar.SetTotal(int64(len(evt.Progress)), true)
   301  					delete(progress, evt.Ref.String())
   302  				}
   303  			}
   304  		}
   305  
   306  		if len(progress) == 0 {
   307  			p.Wait()
   308  			p = mpb.New(mpb.WithWidth(80), mpb.WithOutput(w))
   309  		}
   310  		return nil
   311  	},
   312  		event.ETDatasetSaveStarted,
   313  		event.ETDatasetSaveProgress,
   314  		event.ETDatasetSaveCompleted,
   315  
   316  		event.ETRemoteClientPushVersionProgress,
   317  		event.ETRemoteClientPushVersionCompleted,
   318  
   319  		event.ETRemoteClientPullVersionProgress,
   320  		event.ETRemoteClientPullVersionCompleted,
   321  	)
   322  }
   323  
   324  func addBar(p *mpb.Progress, total int64, title string) *mpb.Bar {
   325  	return p.AddBar(100,
   326  		mpb.PrependDecorators(
   327  			// display our name with one space on the right
   328  			decor.Name(title, decor.WC{W: len(title) + 1, C: decor.DidentRight}),
   329  			// replace ETA decorator with "done" message, OnComplete event
   330  			decor.OnComplete(
   331  				decor.AverageETA(decor.ET_STYLE_GO, decor.WC{W: 4}), "done",
   332  			),
   333  		))
   334  }
   335  
   336  func addElapsedBar(p *mpb.Progress, total int64, title string) *mpb.Bar {
   337  	return p.AddBar(100,
   338  		mpb.PrependDecorators(
   339  			// display our name with one space on the right
   340  			decor.Name(title, decor.WC{W: len(title) + 1, C: decor.DidentRight}),
   341  			// replace ETA decorator with "done" message, OnComplete event
   342  			decor.OnComplete(
   343  				decor.Elapsed(decor.ET_STYLE_GO, decor.WC{W: 4}), "done",
   344  			),
   345  		))
   346  }
   347  
   348  func refString(ref dsref.Ref) string {
   349  	ref.ProfileID = ""
   350  	ref.InitID = ""
   351  	return ref.String()
   352  }