1
16
17
21
22
23 package log
24
25 import (
26 "fmt"
27 "io"
28 "os"
29 "runtime"
30 "strings"
31 "sync"
32
33 "github.com/fatih/color"
34 "github.com/mattn/go-colorable"
35 "github.com/spf13/pflag"
36 "golang.org/x/term"
37
38 "github.com/redhat-developer/odo/pkg/log/fidget"
39 "github.com/redhat-developer/odo/pkg/version"
40 )
41
42
43 const suffixSpacing = " "
44 const prefixSpacing = " "
45
46 var mu sync.Mutex
47 var colors = []color.Attribute{color.FgRed, color.FgGreen, color.FgYellow, color.FgBlue, color.FgMagenta, color.FgCyan, color.FgWhite}
48 var colorCounter = 0
49
50
51
52 type Status struct {
53 spinner *fidget.Spinner
54 status string
55 warningStatus string
56 writer io.Writer
57 }
58
59
60 func NewStatus(w io.Writer) *Status {
61 spin := fidget.NewSpinner(w)
62 s := &Status{
63 spinner: spin,
64 writer: w,
65 }
66 return s
67 }
68
69
70
71
72
73 func IsTerminal(w io.Writer) bool {
74 if runtime.GOOS == "windows" {
75 return true
76 } else if v, ok := (w).(*os.File); ok {
77 return term.IsTerminal(int(v.Fd()))
78 }
79 return false
80 }
81
82
83 func (s *Status) WarningStatus(status string) {
84 s.warningStatus = status
85 s.updateStatus()
86 }
87
88
89
90 func (s *Status) updateStatus() {
91 mu.Lock()
92 if s.warningStatus != "" {
93 yellow := color.New(color.FgYellow).SprintFunc()
94
95
96 warningSubstring := fmt.Sprintf(" [%s %s]", yellow(getWarningString()), yellow(s.warningStatus))
97
98
99 newSuffix := fmt.Sprintf(suffixSpacing+"%s", s.status)
100 newSuffix = truncateSuffixIfNeeded(newSuffix, s.writer, len(warningSubstring))
101
102
103 s.spinner.SetSuffix(fmt.Sprintf("%s%s", newSuffix, warningSubstring))
104 } else {
105 newSuffix := fmt.Sprintf(suffixSpacing+"%s", s.status)
106 s.spinner.SetSuffix(truncateSuffixIfNeeded(newSuffix, s.writer, 0))
107 }
108 mu.Unlock()
109 }
110
111
112
113 func (s *Status) Start(status string, debug bool) {
114 s.End(true)
115
116
117 isTerm := IsTerminal(s.writer)
118 s.status = status
119
120
121
122
123 if !IsJSON() {
124 if !isTerm || debug {
125 fmt.Fprintf(s.writer, prefixSpacing+getSpacingString()+suffixSpacing+"%s ...\n", s.status)
126 } else {
127 s.spinner.SetPrefix(prefixSpacing)
128 newSuffix := fmt.Sprintf(suffixSpacing+"%s", s.status)
129 s.spinner.SetSuffix(truncateSuffixIfNeeded(newSuffix, s.writer, 0))
130 s.spinner.Start()
131 }
132 }
133 }
134
135
136
137 func truncateSuffixIfNeeded(suffix string, w io.Writer, padding int) string {
138
139 terminalWidth := getTerminalWidth(w)
140 if terminalWidth == nil {
141 return suffix
142 }
143
144
145 const additionalPadding = 10
146
147 maxWidth := *terminalWidth - padding - additionalPadding
148
149
150 if maxWidth <= 20 {
151 return suffix
152 }
153
154
155 if len(suffix) <= maxWidth {
156 return suffix
157 }
158
159
160 abbrevSuffix := "..."
161 maxWidth -= len(abbrevSuffix)
162
163
164 suffix = suffix[:maxWidth] + abbrevSuffix
165
166 return suffix
167 }
168
169 func getTerminalWidth(w io.Writer) *int {
170
171 if runtime.GOOS != "windows" {
172
173 if v, ok := (w).(*os.File); ok {
174 w, _, err := term.GetSize(int(v.Fd()))
175 if err == nil {
176 return &w
177 }
178 }
179
180 }
181
182 return nil
183 }
184
185
186
187 func (s *Status) End(success bool) {
188 if s.status == "" {
189 return
190 }
191
192 isTerm := IsTerminal(s.writer)
193 if isTerm {
194 s.spinner.Stop()
195 if !IsJSON() {
196 fmt.Fprint(s.writer, "\r")
197 }
198 }
199
200 if !IsJSON() {
201
202 time := ""
203 if s.spinner.TimeSpent() != "" {
204 time = fmt.Sprintf("[%s]", s.spinner.TimeSpent())
205 }
206
207 if success {
208
209 s.WarningStatus("")
210 green := color.New(color.FgGreen).SprintFunc()
211 fmt.Fprintf(s.writer, prefixSpacing+"%s"+suffixSpacing+"%s %s\n", green(getSuccessString()), s.status, time)
212 } else {
213 red := color.New(color.FgRed).SprintFunc()
214 if s.warningStatus != "" {
215 fmt.Fprintf(s.writer, prefixSpacing+"%s"+suffixSpacing+"%s %s [%s]\n", red(getErrString()), s.status, time, s.warningStatus)
216 } else {
217 fmt.Fprintf(s.writer, prefixSpacing+"%s"+suffixSpacing+"%s %s\n", red(getErrString()), s.status, time)
218 }
219 }
220 }
221
222 s.status = ""
223 }
224
225
226 func (s *Status) EndWithStatus(status string, success bool) {
227 if status == "" {
228 return
229 }
230 s.status = status
231 s.End(success)
232 }
233
234
235
236 func Printf(format string, a ...interface{}) {
237 if !IsJSON() {
238 fmt.Fprintf(GetStdout(), "%s%s%s%s\n", prefixSpacing, getSpacingString(), suffixSpacing, fmt.Sprintf(format, a...))
239 }
240 }
241
242
243
244 func Fprintf(w io.Writer, format string, a ...interface{}) {
245 if !IsJSON() {
246 fmt.Fprintf(w, "%s%s%s%s\n", prefixSpacing, getSpacingString(), suffixSpacing, fmt.Sprintf(format, a...))
247 }
248 }
249
250
251 func Println() {
252 if !IsJSON() {
253 fmt.Fprintln(GetStdout())
254 }
255 }
256
257
258 func Fprintln(w io.Writer) {
259 if !IsJSON() {
260 fmt.Fprintln(w)
261 }
262 }
263
264
265
266 func Success(a ...interface{}) {
267 if !IsJSON() {
268 green := color.New(color.FgGreen).SprintFunc()
269 fmt.Fprintf(GetStdout(), "%s%s%s%s", prefixSpacing, green(getSuccessString()), suffixSpacing, fmt.Sprintln(a...))
270 }
271 }
272
273
274
275
276 func Successf(format string, a ...interface{}) {
277 if !IsJSON() {
278 green := color.New(color.FgGreen).SprintFunc()
279 fmt.Fprintf(GetStdout(), "%s%s%s%s\n", prefixSpacing, green(getSuccessString()), suffixSpacing, fmt.Sprintf(format, a...))
280 }
281 }
282
283
284
285
286 func Warning(a ...interface{}) {
287 Fwarning(GetStderr(), a...)
288 }
289
290
291
292
293 func Fwarning(out io.Writer, a ...interface{}) {
294 if !IsJSON() {
295 Fwarningf(out, "%s", a...)
296 }
297 }
298
299
300
301
302 func Warningf(format string, a ...interface{}) {
303 Fwarningf(GetStderr(), format, a...)
304 }
305
306
307
308
309 func Fwarningf(w io.Writer, format string, a ...interface{}) {
310 if !IsJSON() {
311 yellow := color.New(color.FgYellow).SprintFunc()
312 fullMessage := fmt.Sprintf("%s%s%s", getWarningString(), suffixSpacing, fmt.Sprintf(format, a...))
313 fmt.Fprintln(w, yellow(wrapWarningMessage(fullMessage)))
314 }
315 }
316
317 func wrapWarningMessage(fullMessage string) string {
318 if fullMessage == "" || strings.TrimSpace(fullMessage) == "" {
319 return fullMessage
320 }
321 split := strings.Split(fullMessage, "\n")
322 max := 0
323 for _, s := range split {
324 if len(s) > max {
325 max = len(s)
326 }
327 }
328 h := strings.Repeat("=", max)
329 return fmt.Sprintf(`%[1]s
330 %[2]s
331 %[1]s`, h, fullMessage, h)
332 }
333
334
335
336
337 func Fsuccess(out io.Writer, a ...interface{}) {
338 if !IsJSON() {
339 green := color.New(color.FgGreen).SprintFunc()
340 fmt.Fprintf(out, "%s%s%s%s", prefixSpacing, green(getSuccessString()), suffixSpacing, fmt.Sprintln(a...))
341 }
342 }
343
344
345 func DisplayExperimentalWarning() {
346 if !IsJSON() {
347 msg := `Experimental mode enabled. Use at your own risk.
348 More details on https://odo.dev/docs/user-guides/advanced/experimental-mode`
349 Fwarningf(GetStdout(), msg)
350 }
351 }
352
353
354
355
356
357
358
359
360
361
362 func Title(firstLine, secondLine string) {
363 if !IsJSON() {
364 fmt.Fprint(GetStdout(), Stitle(firstLine, secondLine))
365 }
366 }
367
368
369 func Stitle(firstLine, secondLine string) string {
370 var versionMsg string
371 if version.VERSION != "" {
372 versionMsg = "odo version: " + version.VERSION
373 }
374 if version.GITCOMMIT != "" {
375 versionMsg += " (" + version.GITCOMMIT + ")"
376 }
377 return StitleWithVersion(firstLine, secondLine, versionMsg)
378 }
379
380
381 func StitleWithVersion(firstLine, secondLine, versionLine string) string {
382 blue := color.New(color.FgBlue).SprintFunc()
383 return fmt.Sprintf(` __
384 / \__ %s
385 \__/ \ %s
386 / \__/ %s
387 \__/%s`, blue(firstLine), secondLine, versionLine, "\n")
388 }
389
390
391
392 func Sectionf(format string, a ...interface{}) {
393 if !IsJSON() {
394 blue := color.New(color.FgBlue).Add(color.Underline).SprintFunc()
395 if runtime.GOOS == "windows" {
396 fmt.Fprintf(GetStdout(), "\n- %s\n", blue(fmt.Sprintf(format, a...)))
397 } else {
398 fmt.Fprintf(GetStdout(), "\n↪ %s\n", blue(fmt.Sprintf(format, a...)))
399 }
400 }
401 }
402
403
404
405 func Section(a ...interface{}) {
406 if !IsJSON() {
407 blue := color.New(color.FgBlue).Add(color.Underline).SprintFunc()
408 if runtime.GOOS == "windows" {
409 fmt.Fprintf(GetStdout(), "\n- %s", blue(fmt.Sprintln(a...)))
410 } else {
411 fmt.Fprintf(GetStdout(), "\n↪ %s", blue(fmt.Sprintln(a...)))
412 }
413 }
414 }
415
416
417
418
419 func Deprecate(what, nextAction string) {
420 if !IsJSON() {
421 yellow := color.New(color.FgYellow).SprintFunc()
422 msg1 := fmt.Sprintf("%s%s%s%s%s", yellow(getWarningString()), suffixSpacing, yellow(fmt.Sprintf("%s Deprecated", what)), suffixSpacing, nextAction)
423 fmt.Fprintf(GetStderr(), " %s\n", msg1)
424 }
425 }
426
427
428
429 func Errorf(format string, a ...interface{}) {
430 if !IsJSON() {
431 red := color.New(color.FgRed).SprintFunc()
432 fmt.Fprintf(GetStderr(), " %s%s%s\n", red(getErrString()), suffixSpacing, fmt.Sprintf(format, a...))
433 }
434 }
435
436
437
438 func Ferrorf(w io.Writer, format string, a ...interface{}) {
439 if !IsJSON() {
440 red := color.New(color.FgRed).SprintFunc()
441 fmt.Fprintf(w, " %s%s%s\n", red(getErrString()), suffixSpacing, fmt.Sprintf(format, a...))
442 }
443 }
444
445
446
447 func Error(a ...interface{}) {
448 if !IsJSON() {
449 red := color.New(color.FgRed).SprintFunc()
450 fmt.Fprintf(GetStderr(), "%s%s%s%s", prefixSpacing, red(getErrString()), suffixSpacing, fmt.Sprintln(a...))
451 }
452 }
453
454
455
456 func Ferror(w io.Writer, a ...interface{}) {
457 if !IsJSON() {
458 red := color.New(color.FgRed).SprintFunc()
459 fmt.Fprintf(w, "%s%s%s%s", prefixSpacing, red(getErrString()), suffixSpacing, fmt.Sprintln(a...))
460 }
461 }
462
463
464
465
466 func Info(a ...interface{}) {
467 if !IsJSON() {
468 bold := color.New(color.Bold).SprintFunc()
469 fmt.Fprintf(GetStdout(), "%s", bold(fmt.Sprintln(a...)))
470 }
471 }
472
473
474
475
476 func Infof(format string, a ...interface{}) {
477 if !IsJSON() {
478 bold := color.New(color.Bold).SprintFunc()
479 fmt.Fprintf(GetStdout(), "%s\n", bold(fmt.Sprintf(format, a...)))
480 }
481 }
482
483
484
485
486
487
488
489 func Finfof(w io.Writer, format string, a ...interface{}) {
490 if !IsJSON() {
491 bold := color.New(color.Bold).SprintFunc()
492
493 if runtime.GOOS == "windows" {
494 fmt.Fprintf(w, "%s\n", fmt.Sprintf(format, a...))
495 } else {
496 fmt.Fprintf(w, "%s\n", bold(fmt.Sprintf(format, a...)))
497 }
498
499 }
500 }
501
502
503 func Sbold(s string) string {
504 bold := color.New(color.Bold).SprintFunc()
505 return bold(fmt.Sprint(s))
506 }
507
508
509 func Bold(s string) {
510 if !IsJSON() {
511 bold := color.New(color.Bold).SprintFunc()
512 fmt.Fprintf(GetStdout(), "%s\n", bold(fmt.Sprintln(s)))
513 }
514 }
515
516
517 func SboldColor(c color.Attribute, s string) string {
518 chosenColor := color.New(c).SprintFunc()
519 return chosenColor(fmt.Sprintln(Sbold(s)))
520 }
521
522
523
524
525 func Describef(title string, format string, a ...interface{}) {
526 if !IsJSON() {
527 bold := color.New(color.Bold).SprintFunc()
528 fmt.Fprintf(GetStdout(), "%s%s\n", bold(title), fmt.Sprintf(format, a...))
529 }
530 }
531
532
533
534
535 func Spinner(status string) *Status {
536 return ExplicitSpinner(status, false)
537 }
538
539
540
541
542
543 func Spinnerf(format string, a ...interface{}) *Status {
544 s := NewStatus(GetStdout())
545 s.Start(fmt.Sprintf(format, a...), IsDebug())
546 return s
547 }
548
549
550
551
552
553 func Fspinnerf(w io.Writer, format string, a ...interface{}) *Status {
554 s := NewStatus(w)
555 s.Start(fmt.Sprintf(format, a...), IsDebug())
556 return s
557 }
558
559
560 func SpinnerNoSpin(status string) *Status {
561 return ExplicitSpinner(status, true)
562 }
563
564
565 func ExplicitSpinner(status string, preventSpinning bool) *Status {
566 doNotSpin := true
567 if !preventSpinning {
568 doNotSpin = IsDebug()
569 }
570 s := NewStatus(GetStdout())
571 s.Start(status, doNotSpin)
572 return s
573 }
574
575
576
577 func IsJSON() bool {
578
579 flag := pflag.Lookup("o")
580 if flag != nil && flag.Changed {
581 return strings.Contains(pflag.Lookup("o").Value.String(), "json")
582 }
583
584 return false
585 }
586
587
588 func IsDebug() bool {
589
590 flag := pflag.Lookup("v")
591
592 if flag != nil {
593 return !strings.Contains(pflag.Lookup("v").Value.String(), "0")
594 }
595
596 return false
597 }
598
599
600 func IsAppleSilicon() bool {
601 return runtime.GOOS == "darwin" && (strings.HasPrefix(runtime.GOARCH, "arm") || strings.HasPrefix(runtime.GOARCH, "arm64"))
602 }
603
604
605
606
607 func GetStdout() io.Writer {
608 if runtime.GOOS == "windows" {
609 return colorable.NewColorableStdout()
610 }
611 return os.Stdout
612 }
613
614
615
616
617 func GetStderr() io.Writer {
618 if runtime.GOOS == "windows" {
619 return colorable.NewColorableStderr()
620 }
621 return os.Stderr
622 }
623
624
625
626
627 func getErrString() string {
628 if runtime.GOOS == "windows" {
629 return "X"
630 }
631 return "✗"
632 }
633
634
635
636
637 func getWarningString() string {
638 if runtime.GOOS == "windows" {
639 return "!"
640 }
641 return "⚠"
642 }
643
644
645
646
647 func getSuccessString() string {
648 if runtime.GOOS == "windows" {
649 return "V"
650 }
651 return "✓"
652 }
653
654
655
656
657 func getSpacingString() string {
658 if runtime.GOOS == "windows" {
659 return "-"
660 }
661 return "•"
662 }
663
664
665
666 func ColorPicker() color.Attribute {
667 colorCounter++
668 return colors[(colorCounter)%len(colors)]
669 }
670
View as plain text