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 }